Saving pre-loaded values in memory - Swift/SwiftUI - arrays

I have a class of "questions" - basically cards that store text, a color, and a few other properties.
One of these properties is a favorite button. I have it set as a class so that a user can click on a heart icon to favorite that question.
This is persisting through that version of the app, but every time I restart the app, all my favorites disappear. There is no user data stored on the app, just this list of which questions are favorited. Do I have to use UserDefaults in order to store that data? Or is there some other way I can get that to persist across launches of the app (and also even if they update the version of the app?)
Here is the class:
class Question: Hashable, Identifiable, Codable, Equatable {
static func == (lhs: Question, rhs: Question) -> Bool {
let areEqual = lhs.id == rhs.id
return areEqual
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
var id = UUID()
var text: String
var color: String
var foregroundColor: String?
var submittedBy: String?
var favorited: Bool = false
init (text: String, color: String, submittedBy: String? = "") {
self.text = text
self.color = color
}
}
And an example variable with a "deck" of questions:
var whoQs: [Question] = [
Question(text: "Who are you with when you feel the best?", color: "Who", submittedBy: "Testing Person"),
Question(text: "Who do you let inform you?", color: "Who"),
Question(text: "Who do you miss right now?", color: "Who")
]
And here is how I'm toggling the favorites item:
Button(action: {
currentQuestions.last?.favorited.toggle()
print("\(currentQuestions.last?.text) is favorited: \(currentQuestions.last?.favorited)")
}, label: {
Image(systemName: currentQuestions.last?.favorited ?? false ? "heart.fill" : "heart")
// .foregroundColor(.black)
.imageScale(.large)
}).padding()
.disabled(!shareQuestionVisible)

Related

.onChange shows previous change instead of present and fires only after second trigger

I'm new to Swift and can't understand what's wrong with my code:
#State var selectedCourse: String = "1"
#State var napravlenie: [String] = [""]
var body: some View {
NavigationView {
ZStack{
VStack{
Text("Выберите вашу группу")
.padding(.bottom,80)
.font(.system(size: 30, weight: .semibold))
Text("Курс")
.font(.system(size: 28, weight: .bold))
.frame(width: 370,alignment: .leading)
//.padding()
Menu {
Picker(selection: $selectedCourse, label: EmptyView()) {
ForEach(course, id: \.self) {option in
Text (option)
.tag (1)
}
}
} label: {
customLabel_1
} .onChange(of: selectedCourse) { newValue in
napravlenie = viewModel.readValue(kyrs: newValue)
}
Text("Направление")
.font(.system(size: 28, weight: .bold))
.frame(width: 370,alignment: .leading)
.padding(.bottom,28)
Menu {
Picker(selection: $selectedNapravlenie, label: EmptyView()) {
ForEach(napravlenie, id: \.self) {option in
Text (option)
.tag (2)
}
}
} label: {
customLabel_2
}
}
}
the thing is that I want to update napravlenie array each time the first Picker is used and than pass that array as a ForEach of the second Picker, but it only updates after second trigger and shows an array that was triggered in the first time. I want the second Picker to update immediately, what should I do?
here's my viewModel code
import Foundation
import FirebaseCore
import FirebaseDatabase
class userViewModel: ObservableObject{
#Published var key = ""
#Published var moreKey = []
#Published var key2 = ""
#Published var moreKey2 = []
#Published var nameKey2 = ""
#Published var nameKey3 = ""
//#Published var nameMoreKey = []
#Published var object: String? = nil
#Published var userName: Any? = []
var ref = Database.database().reference()
// функция которая находит институты, из фб по выбору курса
func readValue(kyrs:String) -> [String]{
ref.child("Курс - " + kyrs).observeSingleEvent(of: .value) { snapshot in
for child in snapshot.children{ //тут он проверяет типо каждый раз нужный курс
let snap = child as! DataSnapshot
self.key = snap.key //все что тут находится я хз
self.moreKey.append(self.key)
print(self.moreKey)
}
}
let newArray: [String] = self.moreKey.compactMap { String(describing: $0) }
self.moreKey.removeAll()
return newArray
}
For example, when i choose anything in first Picker, the second one doesn't update napravlenie array, but after second trigger, the second picker shows array, that should've been presented after first trigger

SwiftUI remove data into a structured array via a button in a ForEach

I'm trying to remove a line in my structured array when user click on the delete button. But as I use a foreach to load all my array lines into a specific subview I don't know how to pass the index of the ForEach into my subview to delete my line...
My code is like this,
ScrollView{
VStack {
ForEach(planeLibrary.testPlane){plane in
ZStack {
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(Color.white)
.shadow(color: Color(Color.RGBColorSpace.sRGB, white: 0, opacity: 0.2), radius: 4)
PlaneCellView(plane: plane, planeLibrary: planeLibrary, line: ???)
}
}
}.padding(.horizontal, 16)
}
And my PlaneCellView :
#State var plane: Plane
#ObservedObject var planeLibrary: PlaneLibrary
var line: Int
var body: some View {
//...
VStack(alignment: .leading) {
Text(plane.planeImat)
.font(.title)
.fontWeight(.bold)
Text(plane.planeType)
HStack{
Text(plane.isSe ? "SE" : "ME")
Text(plane.isNight ? "- Night" : "")
Text(plane.isIfr ? "- IFR" : "")
}
}
Spacer()
Button {
// HERE I don't know how to delete my array line ...
planeLibrary.testPlane.remove(at: line)
} label: {
Image(systemName: "trash.circle")
.foregroundColor(.red)
.font(.system(size: 30))
}
//...
}
My Plane library :
struct Plane: Identifiable{
let id = UUID().uuidString
let planeImat: String
let planeType: String
let isSe: Bool
let isIfr: Bool
let isNight: Bool
let autoID: String
init (planeImat: String, planeType: String, isSe: Bool, isIfr: Bool, isNight: Bool, autoID: String){
self.planeType = planeType
self.planeImat = planeImat
self.isSe = isSe
self.isIfr = isIfr
self.isNight = isNight
self.autoID = autoID
}
init(config: NewPlaneConfig){
self.planeImat = config.imat
self.planeType = config.type
self.isSe = config.isSe
self.isIfr = config.isIfr
self.isNight = config.isNight
self.autoID = config.autoID
}
}
I've already try to add id: \.self as I was able to find on this forum but without any success.
You haven't actually included PlaneLibrary, so I will assume that planeLibrary.testPlane is an array of Plane structs.
There are many ways of solving this, including changing testPlane to be a Dictionary of Plane structs (indexed by id), or if order is important, in an OrderedDictionary (add the swift-collections package to your project and import OrderedCollections in the file where it is used). You could use testPlane.removeValue(at: id) to remove the plane from either type of dictionary.
If you keep it as an array, but your array might be large and you're worried about run-time efficiency, the best thing to do is to change your ForEach to include the index of the planes in the loop.
It would look something like this:
ForEach(Array(planeLibrary.testPlane.enumerated()), id: \.element.id) { index, plane in
// In this code you can use either plane, or index.
...
// UI code
Text(plane.autoID)
...
{ // remove closure
planeLibrary.testPlane.remove(at: index)
}
}
But if the array is of reasonable size, you could keep it as it is now and use testPlane.remove(where:) to find it by id at the time of deletion. The code for this is much simpler and easier to read and understand, so it should probably be your first choice. Optimise for large lists later, if you need.
You can't pass the index in because that will crash the ForEach View. Instead, look up its index using its ID afterwards to remove it, e.g.
class RecipeBox: ObservableObject {
#Published var allRecipes: [Recipe]
#Published var collections: [String]
...
func delete(_ recipe: Recipe) {
delete(recipe.id)
}
func delete(_ id: Recipe.ID) {
if let index = index(for: id) {
allRecipes.remove(at: index)
updateCollectionsIfNeeded()
}
}
...
func index(for id: Recipe.ID) -> Int? {
allRecipes.firstIndex(where: { $0.id == id })
}
...
This sample is from Defining the source of truth using a custom binding (Apple Developer)

Find if a different array has the same element and open an Alert

I know from the question it looks like something that has been already answered on this website before, but please read until the end, because I can't find the answer.
So, I have an Array that contains values of TagsModel:
import SwiftUI
import Combine
class DataManager : Equatable, Identifiable, ObservableObject {
static let shared = DataManager()
#Published var storageTags : [TagsModel] = []
typealias StorageTags = [TagsModel]
//The rest of the code
}
And the TagsModel:
import SwiftUI
import Combine
class TagsModel : Codable, Identifiable, Equatable, ObservableObject {
var id = UUID()
var tagName : String
var value : [ValueModel] = []
init(tagName: String) {
self.tagName = tagName
}
static func == (lhs: TagsModel, rhs: TagsModel) -> Bool {
return lhs.id.uuidString == rhs.id.uuidString
}
}
If you need it, the ValueModel is:
import SwiftUI
import Combine
class ValueModel : Codable, Identifiable, Equatable, ObservableObject, Comparable {
var id = UUID()
var name : String
var notes : String?
var expires : Date?
init(name: String, notes: String?, expires: Date?) {
self.name = name
self.notes = notes
self.expires = expires
}
}
Now what I would like to do is let the user add the elements of type ValueModel to the array Value of each TagsModel (which in English means I would like users to be able to add values inside their belonging tags). I can do all this, but I would like to add a check: if any other TagsModel contains the value that the user is trying to add, show an Alert (since every value can have only one tag). This Alert should be asking the user whether he/she wants to add that value to the selected tag and remove it from the other one, or cancel the action.
What I managed to do up to now is this:
import SwiftUI
struct SelectValuesForTagsView: View {
#ObservedObject var dm: DataManager
var tm: TagsModel
#Binding var showSheetSelectValuesForTagsView : Bool
#State var showAlertValueAlreadyInTag = false
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
#GestureState private var dragOffset = CGSize.zero
#Environment(\.colorScheme) var colorScheme
var body: some View {
NavigationView {
Form {
ForEach(dm.storageValues) { valuesOfForEach in
HStack {
if tm.value.contains(where: { $0.id.uuidString == valuesOfForEach.id.uuidString
}) {
Image(systemName: "checkmark.circle.fill")
.frame(width: 22, height: 22)
.foregroundColor(.green)
} else {
Image(systemName: "circle")
.frame(width: 22, height: 22)
.foregroundColor(colorScheme == .dark ? .white : .black)
}
Button(action: {
if !tm.value.contains(where: { $0.id.uuidString == valuesOfForEach.id.uuidString
}) {
tm.value.append(valuesOfForEach)
dm.save()
} else {
guard let indexValue = tm.value.firstIndex(where: { $0.id.uuidString == valuesOfForEach.id.uuidString
}) else { return }
tm.value.remove(at: indexValue)
dm.save()
}
}, label: {
if tm.value.contains(where: { $0.id.uuidString == valuesOfForEach.id.uuidString
}) {
Text(valuesOfForEach.name)
.foregroundColor(.blue)
} else {
Text(valuesOfForEach.name)
.foregroundColor(.yellow)
}
})
}
}
}
.navigationBarTitle(Text("Add Values"), displayMode: .automatic)
.navigationBarBackButtonHidden(true)
.navigationBarItems(trailing: saveButton)
}
}
var saveButton: some View {
Button(action: {
let tag : TagsModel
if dm.storageTags.contains(where: {$0.value == tm.value}) {
showAlertValueAlreadyInTag = true
//The problem is here, that the Alert ALWAYS shows up, even though there's only this Tag containing that value
}
else {
dm.save()
showSheetSelectValuesForTagsView = false
}
}, label: {
Text("Save")
.foregroundColor(.blue)
}).alert(isPresented: $showAlertValueAlreadyInTag, content: { alertValueAlreadyInTag })
}
var alertValueAlreadyInTag : Alert {
Alert(title: Text("Attention!"), message: Text("Every value can only be assigned to one tag. One or more values you selected are already into another tag. Would you like to substitute the belonging tag for these values?"), primaryButton: Alert.Button.default(Text("Yes, substitute"), action: {
dm.save()
showAlertValueAlreadyInTag = false
showSheetSelectValuesForTagsView = false
}), secondaryButton: Alert.Button.default(Text("Cancel"), action: {
showAlertValueAlreadyInTag = false
}))
}
}
How can I check if another tag inside the storageTags has the already the same value inside of its value Array?

Why doesn't it show me the items added in the array?

Why doesn't it show me the items added in the array? (I want to keep this array layout).
Could someone tell me how should I complete the array to register the elements correctly?
It always returns zero.
Sample Code:
import UIKit
class FilterCategory: Equatable{
static func == (lhs: FilterCategory, rhs: FilterCategory) -> Bool {
return lhs.id == rhs.id
}
var id : Int = 0
var name: String = ""
var image: UIImage = UIImage()
init(id: Int, name: String, image: UIImage) {
self.id = id
self.name = name
self.image = image
}
}
class demo : UIViewController {
var images = [FilterCategory]()
var thumbnailImagesList = [Int: [UIImage?]]()
override func viewDidLoad() {
super.viewDidLoad()
images.append(FilterCategory.init(id: 1, name: "1.1", image: UIImage()))
images.append(FilterCategory.init(id: 1, name: "1.2", image: UIImage()))
images.append(FilterCategory.init(id: 2, name: "2.1", image: UIImage()))
images.append(FilterCategory.init(id: 2, name: "2.2", image: UIImage()))
var index = 0
for img in images {
self.thumbnailImagesList[img.id]?[index] = UIImage()
index = index+1
}
print(thumbnailImagesList.count)
}
}
The dictionary is empty from start but you are treating it like it contains values, use the below code instead that will either add the image object to a new or existing array depending on if the key (img.id) exists or not in the dictionary
for img in images {
self.thumbnailImagesList[img.id, default:[]].append(UIImage())
}
A question is why you are creating new image objects everywhere in your code, maybe you meant
for img in images {
self.thumbnailImagesList[img.id, default:[]].append(img.image)
}

Issues with transparent PNG in UIActivityViewController for FB Messenger and iMessage

I have been using the following code to call the UIActivityViewController for sharing stickers via the various social media apps:
if let image = sticker.getUIImage(), let imgData = UIImagePNGRepresentation(image) {
let activityVC = UIActivityViewController(activityItems: [imgData], applicationActivities: nil)
activityVC.popoverPresentationController?.sourceView = gesture.view
self.present(activityVC, animated: true, completion: nil)
} else {
ErrorHandler.handleError(STICKER_IMAGE_NOT_FOUND, sticker)
}
This code has been working fine until the most recent update to FB messenger (version 98.0). Now it shows an error "Couldn't load content". FB messenger appears to prefer a URL like this:
if let item = sticker.getImageURL() {
let activityVC = UIActivityViewController(activityItems: [item], applicationActivities: nil)
activityVC.popoverPresentationController?.sourceView = gesture.view
self.present(activityVC, animated: true, completion: nil)
} else {
ErrorHandler.handleError(STICKER_IMAGE_NOT_FOUND, sticker)
}
This works fine with FB Messenger but iMessage displays the transparent PNG with a black background.
I was looking at UIActivityViewControllerCompletionWithItemsHandler but the discussion states it runs after the activity, too late for what I need to do. I also tried creating a custom UIActivity returning UIActivityType.message for activityType but it was added on to the bottom of the controller rather than taking over the default.
Is there a way to intercept the selection of the item in UIActivityViewController so I can use the MFMessageComposeViewController and add the UIImagePNGRepresentation to the message and allow all the others to use the URL?
Is there a particular argument type that I can pass to UIActivityViewController that will correctly display transparent PNG with all the social apps?
TIA
Mike
Circling back to close this up. I eventually switched from using the UIActivityViewController universally to a system that customizes what is done for each type of service. I use BDGShare by Bob de Graaf. I build a list of the services that the app supports, show a button for each and then a switch to jump to each type of share. Just in case someone's working on this, here's what I came up with:
The types of sharing the app wants to support:
public enum ShareServiceType:Int {
case iMessage=0, facebook, twitter, instagram, whatsApp, facebookMessenger, email, more
}
A class to store information about the type of sharing service
public class ShareTargetVO: NSObject
{
var icon:String!
var label:String!
var type:ShareServiceType
init( serviceType:ShareServiceType, icon:String, label:String )
{
self.type = serviceType
self.icon = icon
self.label = label
}
}
A function in my social networking helper to populate the list of available services:
static func getShareTargetList(for sticker : ReeSticker) -> [ShareTargetVO] {
var services:Array<ShareTargetVO> = []
// Check to see which capabilities are present and add buttons
if (BDGShare.shared().isAvailable(forServiceType: SLServiceTypeFacebook)) {
services.append(ShareTargetVO(serviceType: ShareServiceType.facebook, icon: "Icon-Share-Facebook", label: "Facebook"))
}
// Checking for facebook service type because there's no availability check with FBSDK for messenger (could be rolled into the lib)
if (BDGShare.shared().isAvailable(forServiceType: SLServiceTypeFacebook) && !sticker.type.doesContain("video")) {
services.append(ShareTargetVO(serviceType: ShareServiceType.facebookMessenger, icon: "Icon-Share-Messenger", label: "Messenger"))
}
if (BDGShare.shared().isAvailable(forServiceType: SLServiceTypeTwitter)) {
services.append(ShareTargetVO(serviceType: ShareServiceType.twitter, icon: "Icon-Share-Twitter", label: "Twitter"))
}
if (BDGShare.shared().canSendSMS()) {
services.append(ShareTargetVO(serviceType: ShareServiceType.iMessage, icon: "Icon-Share-Messages", label: "Messages"))
}
if UIApplication.shared.canOpenURL(URL(string: "whatsapp://")! as URL) && !sticker.type.contains("video") {
services.append(ShareTargetVO(serviceType: ShareServiceType.whatsApp, icon: "Icon-Share-Whatsapp", label: "What's App?"))
}
if UIApplication.shared.canOpenURL(URL(string: "instagram://app")! as URL) && !sticker.type.contains("video") {
services.append(ShareTargetVO(serviceType: ShareServiceType.instagram, icon: "Icon-Share-Instagram", label: "Instagram"))
}
if (BDGShare.shared().canSendEmail()) {
services.append(ShareTargetVO(serviceType: ShareServiceType.email, icon: "Icon-Share-Mail", label: "Email"))
}
services.append(ShareTargetVO(serviceType: ShareServiceType.more, icon: "Icon-Share-More", label: "More"))
return services
}
A function in my view controller to populate a UICollectionView of buttons for sharing limited to those services that are returned from the list function:
func layoutShareButtons() {
let f = self.view.frame
let btnWidth = f.width * 0.82
let bannerWidth = btnWidth + 10
mask = UIView(frame: CGRect(x: 0, y: 0, width: f.width, height: f.height))
mask.backgroundColor = .black
mask.alpha = 0.3
self.view.addSubview(mask)
buttonList = SocialHelper.getShareTargetList(for: self.sticker)
let buttonGridLayout = UICollectionViewFlowLayout()
buttonGridLayout.sectionInset = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
buttonGridLayout.itemSize = CGSize(width: 60, height: 60)
buttonGridLayout.scrollDirection = .horizontal
buttonListView = UICollectionView(frame: CGRect(x: (f.width - bannerWidth) / 2,
y: self.preview.frame.origin.y + self.preview.frame.height + 10,
width: bannerWidth,
height: 80),
collectionViewLayout: buttonGridLayout)
buttonListView.register(ShareButtonCell.self, forCellWithReuseIdentifier: "shareButtonCell")
buttonListView.dataSource = self
buttonListView.delegate = self
self.view.addSubview(buttonListView)
// cancel button for sharing view
// Button (added last to ensure it's on top of the z-order)
cancelButton = SimpleButton(frame: CGRect(x: (f.width - bannerWidth) / 2, y: self.buttonListView.frame.origin.y + self.buttonListView.frame.height + 10, width: bannerWidth, height: 52))
cancelButton.backgroundColor = UIColor(netHex:0x202020)
cancelButton.layoutComponent(0, label: "Cancel")
cancelButton.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.cancelButtonPressed(_:))))
self.view.addSubview(self.cancelButton)
}
UICollectionView didSelect handler (for better SoC depending on your app remove the "share" function to a separate class just for the share implementation, in this app the screen we're working with is specifically for share):
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
self.share(shareTarget: buttonList[indexPath.row])
}
Finally, a function call the correct share type:
func share(shareTarget: ShareTargetVO) {
// Params to submit to service
self.shareUrl = self.sticker.getStickerURL()
let textStr = "" // BDGShare supports a message passed in as well but we just send the sticker
// we need the NSData either way (sticker / video)
var ok = true
// try? is fine here because result is tested below
let urlData : Data? = try? Data(contentsOf: self.shareUrl as URL)
ok = (urlData != nil)
var img: UIImage? = nil
// if it's an image type then get it
if ok {
if (self.shareUrl.pathExtension.contains("png")) || (self.shareUrl.pathExtension.contains("jpg")) {
img = UIImage(data: urlData! as Data)
ok = (img != nil)
}
}
if !ok {
let alertCtrl = UIAlertController(title: "Error sending", message: "There was an error gathering the information for sending this sticker.", preferredStyle: .alert)
alertCtrl.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: nil))
self.present(alertCtrl, animated: true, completion: nil)
return
}
switch shareTarget.type
{
case .iMessage:
BDGShare.shared().shareSMS(textStr, recipient: nil, image: img, data:urlData! as Data, imageName: "sendSticker.png", completion: {(SharingResult) -> Void in
// Handle share result...
self.handleShareResult(shareTarget.type, shareResult: SharingResult)
})
break
case .facebook:
BDGShare.shared().shareFacebook("", urlStr: "", image: img, completion: {(SharingResult) -> Void in
// Handle share result...
self.handleShareResult(shareTarget.type, shareResult: SharingResult)
})
break
case .twitter:
... more code for handling each type of share
}
}
So there are all the pieces I used to implement using BDGShare to get around the UIActivityViewController. BTW - the "More" option at the end of the list calls that UIActivityViewController so it's still available to the user if they so choose.
HTH, Mike

Resources