This shows you the differences between two versions of the page.
| Both sides previous revision Previous revision Next revision | Previous revision | ||
| swift [2022/08/07 23:37] admin | swift [2023/11/19 22:16] (current) admin | ||
|---|---|---|---|
| Line 3: | Line 3: | ||
| Tutorial for beginners: https://developer.apple.com/tutorials/app-dev-training | Tutorial for beginners: https://developer.apple.com/tutorials/app-dev-training | ||
| - | * Important concept: pass object between views. | + | Important concept: pass object between views. | 
| - | * `struct ScrumsView_Previews: PreviewProvider` is just for showing an mock up preview when you coding. It is not a part of the real app. | + | |
| - | * Add `@State` at the beginning to declear a single source of truth at the very most parent view. Use `$[variable name]` to pass it to child view. In child view initialize with `@Binding var [variable name]: [variable type]` to use that data. | + | `struct ScrumsView_Previews: PreviewProvider` is just for showing an mock up preview when you coding. It is not a part of the real app. | 
| - | * The annotation `environmentObject(_:)` means ... | + | |
| - | * Property wrappers is those code started with `@` than you add before declearing an variable or object. For example @EnvironmentObject var time: Float | + | Add `@State` at the beginning to declear a single source of truth at the very most parent view. Use `$[variable name]` to pass it to child view. In child view initialize with `@Binding var [variable name]: [variable type]` to use that data. | 
| + | |||
| + | |||
| + | The annotation `environmentObject(_:)` means ... | ||
| + | |||
| + | Property wrappers is those code started with `@` than you add before declearing an variable or object. For example `@EnvironmentObject var time: Float`. | ||
| + | |||
| + | Scenes are like windows. active — A scene is in the foreground, and the user can interact with it. | ||
| + | inactive — The scene is visible, but the system disabled interaction with the scene. For example, in multitasking mode, you can see your app’s panel alongside others, but it isn’t the active panel. | ||
| + | background — The app is running, but the scene isn’t visible in the UI. A scene enters this phase prior to app termination. A common use is to trigger an action that saves app data when the scene phase becomes inactive. | ||
| + | |||
| + | Task modifier for async loading contents in the background. [ref](https://www.hackingwithswift.com/quick-start/swiftui/how-to-run-an-asynchronous-task-when-a-view-is-shown). Use continuations to bridge between **asynchronous functions** and existing **callback-based function**s. | ||
| + | |||
| + | ## Class | ||
| + | |||
| + | ### @Published wrapper | ||
| + | In this code, Why there needs to be a @Published wrapper? | ||
| + | |||
| + | ``` | ||
| + | class MyStore: ObservableObject { | ||
| + | @Published var selection:Int? | ||
| + | |||
| + | func sendID(_ id: Int) { | ||
| + | self.selection = id | ||
| + | } | ||
| + | } | ||
| + | ``` | ||
| + | |||
| + | The @Published wrapper is used in SwiftUI to automatically notify any views that are observing the property when its value changes. | ||
| + | |||
| + | In the given code, the selection property is marked as @Published, indicating that it is an observable property. When the sendID(_:) function is called and the value of selection is updated with a new ID, the @Published wrapper will automatically send notifications to any views that are observing this property. These views will then be updated accordingly to reflect the new value of selection. | ||
| + | |||
| + | By using the @Published wrapper, you eliminate the need to manually send notifications and manage observers, making it easier to update and synchronize data between different parts of your SwiftUI app. | ||
| + | |||
| + | |||
| + | ``` | ||
| + | struct ClosureDemo: View { | ||
| + | @StateObject var store = MyStore() | ||
| + | var body: some View { | ||
| + | if let currentID = store.selection { // currentID won't be updated if there is no @Publish wrapper in MyStore class | ||
| + | Text("Current ID: \(currentID)") | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | |||
| + | class MyStore: ObservableObject { | ||
| + | @Published var selection:Int? | ||
| + | |||
| + | func sendID(_ id: Int) { | ||
| + | self.selection = id | ||
| + | } | ||
| + | } | ||
| + | ``` | ||
| + | |||
| + | ## Data Storage | ||
| + | |||
| + | ### UserDefaults | ||
| + | is used to store small user preferences and application settings. It has been available as part of the Foundation framework in iOS and macOS long before SwiftUI was introduced. | ||
| + | |||
| + | ### AppStorage | ||
| + | is just a wrapper of UserDefaults in Swift UI. | ||
| + | |||
| + | ### iCloud Key-Value store (NSUbiquitousKeyValueStore) | ||
| + | You can think of it like UserDefaults, but the value is shared across different devices for the same app, that ties to an Apple ID account. | ||
| + | |||
| + | |||
| + | ref: | ||
| + | [[https://developer.apple.com/library/archive/documentation/General/Conceptual/iCloudDesignGuide/Chapters/Introduction.html#//apple_ref/doc/uid/TP40012094|Apple: iCloud Design Guide]] | ||
| + | [[https://developer.apple.com/library/archive/documentation/General/Conceptual/iCloudDesignGuide/Chapters/DesigningForKey-ValueDataIniCloud.html#//apple_ref/doc/uid/TP40012094-CH7|Apple: Designing for Key-Value Data in iCloud]] | ||
| + | |||
| + | ### Relationship: Connect entities in CoreData | ||
| + | ref: https://www.hackingwithswift.com/books/ios-swiftui/one-to-many-relationships-with-core-data-swiftui-and-fetchrequest | ||
| + | |||
| + | 1. Create Entities in CoreData | ||
| + | 2. Select the relationship created, in the right tool panel, change `Type` to `To Many` | ||
| + | 3. Select the entity, in the right tool panel, change `Codegen` to `Manual/None` | ||
| + | 4. Use `Editor>Create NSManagedObject SubClass...` menu to create `...CoreDataClass` file and `...CoreDataProperties` file for each entity. | ||
| + | 5. In `...CoreDataProperties` file, you should be able to see code like below for adding transactions to this certain piggy object. | ||
| + | |||
| + | ``` | ||
| + | extension Piggy { | ||
| + | |||
| + | @nonobjc public class func fetchRequest() -> NSFetchRequest<Piggy> { | ||
| + | return NSFetchRequest<Piggy>(entityName: "Piggy") | ||
| + | } | ||
| + | |||
| + | @NSManaged public var capacity: NSDecimalNumber? | ||
| + | @NSManaged public var currentMoney: NSDecimalNumber? | ||
| + | @NSManaged public var dailyAllowance: NSDecimalNumber? | ||
| + | @NSManaged public var id: UUID? | ||
| + | @NSManaged public var lastDepositDay: Date? | ||
| + | @NSManaged public var name: String? | ||
| + | @NSManaged public var totalSaved: NSDecimalNumber? | ||
| + | @NSManaged public var transactions: NSSet? | ||
| + | } | ||
| + | extension Piggy { | ||
| + | @objc(addTransactionsObject:) | ||
| + | @NSManaged public func addToTransactions(_ value: PurchasedItem) | ||
| + | |||
| + | @objc(removeTransactionsObject:) | ||
| + | @NSManaged public func removeFromTransactions(_ value: PurchasedItem) | ||
| + | |||
| + | @objc(addTransactions:) | ||
| + | @NSManaged public func addToTransactions(_ values: NSSet) | ||
| + | |||
| + | @objc(removeTransactions:) | ||
| + | @NSManaged public func removeFromTransactions(_ values: NSSet) | ||
| + | } | ||
| + | ``` | ||
| + | |||
| + | 1. Edit ` ...CoreDataProperties` file, add code like `public var wrappedCapacity: NSDecimalNumber { capacity ?? 0.0 }` | ||
| + | 2. In ` ...CoreDataProperties` file, `transactions` is an `NSSet`, perform the following steps to convert it from an `NSSet` to a `Set<PurchasedItem>` – a Swift-native type where the types of its contents is specificy to `PurchasedItem` type. Convert that `Set<PurchasedItem>` into an array, so that `ForEach` can read individual values from there. Sort that array, so the transactions come in a sensible order. | ||
| + | |||
| + | ``` | ||
| + | public var transactionArray: [PurchasedItem] { | ||
| + | let set = transactions as? Set<PurchasedItem> ?? [] | ||
| + | |||
| + | return set.sorted { | ||
| + | $0.wrappedTimestamp < $1.wrappedTimestamp | ||
| + | } | ||
| + | } | ||
| + | ``` | ||
| + | |||
| + | 1. Use them in your View: To read purchased items connected to a certain piggy in a View. Use the following code: | ||
| + | |||
| + | ``` | ||
| + | struct HistoryView: View { | ||
| + | @Environment(\.managedObjectContext) private var viewContext | ||
| + | @EnvironmentObject private var persistenceController: PersistenceController | ||
| + | let piggy: Piggy | ||
| + | |||
| + | @FetchRequest private var items: FetchedResults<PurchasedItem> | ||
| + | |||
| + | init(piggy: Piggy) { | ||
| + | _items = FetchRequest<PurchasedItem>( | ||
| + | sortDescriptors: [NSSortDescriptor(keyPath: \PurchasedItem.timestamp, ascending: false)], | ||
| + | predicate: NSPredicate(format: "piggyStored == %@", piggy) | ||
| + | ) | ||
| + | self.piggy = piggy | ||
| + | } | ||
| + | |||
| + | var body: some View { | ||
| + | NavigationView { | ||
| + | if (items.count != 0) { | ||
| + | List { | ||
| + | ForEach(items) { item in | ||
| + | // view related code | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | ``` | ||
| + | |||
| + | Or use the following code. However with this method the HistoryView won't update if you add new purchasedItem (transaction) because it is only monitoring the object `piggy`. To trigger view update when purchasedItem is added, you need to use the method above to fetch `PurchasedItem` directly. | ||
| + | |||
| + | ``` | ||
| + | struct HistoryView: View { | ||
| + | @Environment(\.managedObjectContext) private var viewContext | ||
| + | @EnvironmentObject private var persistenceController: PersistenceController | ||
| + | let piggy: Piggy | ||
| + | |||
| + | var body: some View { | ||
| + | NavigationView { | ||
| + | if (piggy.transactionArrary.count != 0) { | ||
| + | List { | ||
| + | ForEach(piggy.transactionArray) { item in | ||
| + | // view related code | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | ``` | ||
| + | |||
| + | 1. To write new purchased item and connect it to piggy. | ||
| + | |||
| + | ```swiftUI | ||
| + | let newPurchase = PurchasedItem(context: viewContext) | ||
| + | newPurchase.id = UUID() | ||
| + | newPurchase.type = 0 // 0: purchase | ||
| + | newPurchase.name = name | ||
| + | newPurchase.price = NSDecimalNumber(decimal: Decimal(price)) | ||
| + | newPurchase.timestamp = Date() | ||
| + | newPurchase.piggyStored = piggy | ||
| + | try? viewContext.save() | ||
| + | ``` | ||
| + | |||
| + | |||
| + | |||
| + | ## Icons | ||
| + | |||
| + | How to import and use custom icons: | ||
| + | |||
| + | 1. Download icons svg that is made in proper format: https://fonts.google.com/icons?icon.style=Rounded&icon.set=Material+Symbols&icon.platform=ios | ||
| + | 2. Import the svg file into Assets in Xcode | ||
| + | 3. Use it in the code, for example: `barItem: UITabBarItem(title: "Piggy", image: UIImage(named: "piggy.saving"))` | ||
| + | |||
| + | |||
| + | |||
| + | ## Quick Reference | ||
| + | {{::swiftui_color_variable.jpg?nolink&400|}} | ||
| + | |||
| + | {{:swiftui_font_variable.png?nolink&600|}} | ||