肇鑫的技术博客

肇鑫 / Owen Zhao

独立开发者,主要开发 iOS、watchOS、macOS 应用。

目前在维护 SleepTapRooster Time,以及 Markdown Writer 相关工具。

最新文章

Get Critical Alert Permission from User instead of Apple

通用

When a user uses "Do not disturb" mode, your notifications no longer get sound and banner, unless your app can post a Critical Alert.

However, to get the entitlement of critical alert, you have to ask Apple to give your permission. Someone complains that their requests to Apple even took months without a response.

Critical Alerts entitlement

I found a new way to get the permission directly from the user, instead of Apple.

Steps

UNUserNotificationCenter.current().requestAuthorization(options: [.sound, .alert, .criticalAlert]) { granted, error in
    guard error == nil else {
        DispatchQueue.main.async {
            NSSound.beep()
            let alert = NSAlert(error: error!)
            alert.runModal()
        }
        
        return
    }
}

Above code will get an error as you don't have the entitlement of critical alert. But we could use code below:

UNUserNotificationCenter.current().requestAuthorization(options: [.sound, .alert]) { granted, error in
    guard error == nil else {
        DispatchQueue.main.async {
            NSSound.beep()
            let alert = NSAlert(error: error!)
            alert.runModal()
        }
        
        return
    }
    
    if granted {
        UNUserNotificationCenter.current().requestAuthorization(options: [.sound, .alert, .criticalAlert]) { granted, error in
            /* You will get an error here as you don't have the critical alert entitlement.
                But since you have already got the permission for normal notifications, you now get a disabled critical alert permission.
                You are just one step to the critical alert. Ask your user to manually enable the permisssion.
                The error should be igonred here.
                */

            UNUserNotificationCenter.current().getNotificationSettings { settings in
                if settings.criticalAlertSetting != .enabled {
                    DispatchQueue.main.async {
                        NSSound.beep()
                        showCriticalAlert = true
                    }
                }
            }
        }
    }
}

We request the permission twice. First, we request the common notification permission. When a user allow that, we request the critical alert permission. Since we have already got the common notification permission the second permission request won't pop up. Also as we don't have critical alert entitlement from Apple, we will get an error. But the error can be ignored. Then when you open the app's notification settings, there is a critical alert permission, with disabled state. So what you only need to do is to show an alert and ask the user to enable the critical alert if the user wants to get notified in "Do not disturb" mode.

Sample App

I have already got an sample app on sale in App Store. You can try it for free.

Stand Reminder

The Login Item in macOS Ventura

macOS

Apps Crash on Ventura

Before Ventura, if I wanted some app to launch when a user logged in, I used this code:

private func setAutoStart() {
    let shouldEnable = Defaults[.autoLaunchWhenLogin]
    
    if !SMLoginItemSetEnabled("com.parussoft.Stand-Reminder-Launcher" as CFString, shouldEnable) {
        fatalError()
    }
}

However, in macOS Ventura, SMLoginItemSetEnabled(_:_:) global function is deprecated, and what's more, it takes no effects so it returns false and fatalError() is executed. So every app of mine that used auto launch when logged in crashed on macOS Ventura.

How to Solve

To solve the issue is easy, we must update the code and use the new API in macOS Ventura and later.

private func setAutoStart() {
    let shouldEnable = Defaults[.autoLaunchWhenLogin]
    
    if #available(macOS 13.0, *) {
        if shouldEnable {
            try? SMAppService().register()
        } else {
            try? SMAppService().unregister()
        }
    } else {
        if !SMLoginItemSetEnabled("com.parussoft.Stand-Reminder-Launcher" as CFString, shouldEnable) {
            fatalError()
        }
    }
}

Other Related Issues

In Ventura, you may find that many versions of the same app that created login items all launched when you logged in. Once I had 3 "Stand Reminder" and 2 "Poster 2" in different versions auto launched when I logged in.

I found that the extra apps were debug versions that I had tested with Xcode. So there must be some relations with the new API.

Workaround

You can simply manually remove the debug version apps to solve the issue. But this method takes to much time as you always have new debug versions.

Or you can stop this feature on debug versions after you fully tested it.

private func setAutoStart() {
    #if !DEBUG
    let shouldEnable = Defaults[.autoLaunchWhenLogin]
    
    if #available(macOS 13.0, *) {
        if shouldEnable {
            try? SMAppService().register()
        } else {
            try? SMAppService().unregister()
        }
    } else {
        if !SMLoginItemSetEnabled("com.parussoft.Stand-Reminder-Launcher" as CFString, shouldEnable) {
            fatalError()
        }
    }
    #endif
}

An Interesting Issue of @State Variable Not Set after Set

SwiftUI

In my recent project, a @State variable was not set after set.

        self.project?.currentTransUnit?.isVerified = true
        self.project?.update() // debug point. 
        // po "self.project?.currentTransUnit?.isVerified" prints false.

So I created another project to narrow the issue.

//
//  Model_SampleApp.swift
//  Model Sample
//
//  Created by zhaoxin on 2022/8/28.
//

import SwiftUI

@main
struct Model_SampleApp: App {
    @State private var foo:Foo? = Foo(bar: Bar(item: Item(name: "Johnny")))
    
    var body: some Scene {
        WindowGroup {
            ContentView(foo: $foo)
        }
    }
}
//
//  ContentView.swift
//  Model Sample
//
//  Created by zhaoxin on 2022/8/28.
//

import SwiftUI

struct ContentView: View {
    @Binding var foo:Foo?
    
    var body: some View {
        VStack {
            Text(foo?.bar.item.name ?? "nil")
            
            HStack {
                TextField("name", text: Binding(get: {
                    foo?.bar.item.name ?? "nil"
                }, set: { newValue in
                    foo?.bar.item.name = newValue
                }))
            }
        }
        .padding()
        .onChange(of: foo) { newValue in
            print(newValue)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(foo: .constant(nil))
    }
}
//
//  Model.swift
//  Model Sample
//
//  Created by zhaoxin on 2022/8/28.
//

import Foundation

struct Foo:Equatable {
    var bar:Bar
}

struct Bar:Equatable {
    var item:Item
}

struct Item:Equatable {
    var name:String
    let id = UUID()
}

All things went fine. I noticed that onChange required Equatable. So I did a test.

func onChange<V>(of value: V, perform action: @escaping (V) -> Void) -> some View where V : Equatable
struct Item:Equatable {
    var name:String
    let id = UUID()
    
    static func == (lhs: Item, rhs: Item) -> Bool {
        return lhs.id == rhs.id
    }
}

Now the issue happened again. So I guess for optimize performance. When set to a @State variable, SwiftUI compared the previous and current value by Equatable. If they were the same, SwiftUI thought they were identical and didn't replace that current with the previous value. So if we implemented the Equatable's method with part implementation on purpose, the issue appeared on the values which were not mentioned in Equatable's method.

Understand TabView in SwiftUI and Adopt a TabView with indicators into Window’s Toolbar

SwiftUI

A Simplest TabView Sample

ContentView

struct ContentView: View {
    var body: some View {
        TabView {
            ForEach(1..<4) { id in
                TabItemView(id: id)
            }
        }
    }
}

TabItemView

struct TabItemView: View {
    @State var id:Int
    
    var body: some View {
        Text("Tab \(id)")
            .tabItem {
                Text(String(id))
            }
    }
}

first_tabview_sample

Life Cycle of Tab Item View

TabItemView

struct TabItemView: View {
    @State var id:Int
    
    var body: some View {
        Text("Tab \(id)")
            .tabItem {
                Text(String(id))
            }
            .onAppear(perform: {
                print("I am tab \(id)")
            })
            .onDisappear {
                print("Tab \(id) am quit!")
            }
    }
}

Runs.

I am tab 1
Tab 1 am quit!
I am tab 2
Tab 2 am quit!
I am tab 3
Tab 3 am quit!
I am tab 1

Is the tab item view just hidden or actually quit when you change a tab?

TabItemView

struct TabItemView: View {
    @State var id:Int
    @State private var title = ""
    
    var body: some View {
        HStack {
            Text("Tab \(id)")
                .onAppear(perform: {
                    print("I am tab \(id)")
                })
                .onDisappear {
                    print("Tab \(id) am quit!")
            }
            
            TextField("Title", text: $title)
        }
        .tabItem {
            Text(String(id))
        }
        .padding()
    }
}

Input "Test" in tab 1, and change to other tabs, then change back to tab 1. You will see the "Test" is still there. That means the tab item view was just hidden, not quit.

third_tabview_sample

Change Tab Name

TabItemView

struct TabItemView: View {
    @State var id:Int
    @State private var title = ""
    
    var body: some View {
        HStack {
            Text("Tab \(id)")
                .onAppear(perform: {
                    print("I am tab \(id)")
                })
                .onDisappear {
                    print("Tab \(id) am quit!")
            }
            
            TextField("Title", text: $title)
        }
        .tabItem {
            Text(getTabName())
        }
        .padding()
    }
    
    private func getTabName() -> String {
        if title.isEmpty {
            return String(id)
        }
        
        return title
    }
}

change_tab_name

Put Indicators in Window's Title Bar

ContentView

@main
struct TabView_SampleApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .windowToolbarStyle(.unifiedCompact(showsTitle: false))
    }
}

tab_in_window_title

The indicators worked. But the tab title no longer worked.

Fix Tab Title

TabItemView

struct TabItemView: View {
    @Binding var tabContent:TabContent
    
    var body: some View {
        HStack {
            Text("Tab \(tabContent.id)")
            
            TextField("Title", text: $tabContent.title)
                .frame(minWidth: 300)
        }
        .tabItem {
            Text(getTabName())
        }
        .padding()
    }
    
    private func getTabName() -> String {
        if tabContent.title.isEmpty {
            return String(tabContent.id)
        }
        
        return tabContent.title
    }
}

struct TabContent:Identifiable, Equatable, Hashable {
    var id = 0
    var title = ""
}

ContentView

struct ContentView: View {
    @State private var tabContents:[TabContent] = {
        (1..<4).map { TabContent(id: $0) }
    }()
    
    @State private var currentTabContent = TabContent(id: 1)
    
    var body: some View {
        ForEach($tabContents) { $tabContent in
            if currentTabContent.id == tabContent.id {
                TabItemView(tabContent: $tabContent)
            }
        }
        .toolbar {
            HStack {
                Picker(String(currentTabContent.title), selection: $currentTabContent) {
                    ForEach(tabContents) { tabContent in
                        Text(tabContent.title.isEmpty ? String(tabContent.id) : tabContent.title).tag(tabContent)
                    }
                }
                .pickerStyle(.segmented)
                .onChange(of: currentTabContent) { newValue in
                    if let index = tabContents.map({$0.id}).firstIndex(of: newValue.id) {
                        tabContents[index] = newValue
                    }
                }
                
                Spacer()
            }
        }
        .frame(width: 400, height: 50)
    }
}

worked_tab_in_window_title

A Little Guide of NSViewControllerRepresentable

SwiftUI

What is NSViewControllerRepresentable

In SwiftUI, there are no View Controllers, only Views. So what should we do if want to use something related to View Controllers? Here comes NSViewControllerRepresentable/UIViewControllerRepresentable. Basically, we can use instance of View Controller Representable as View, contains a view controller.

Since view controllers have their own view, ViewControllerRepresentable has not view body.

protocol NSViewControllerRepresentable : View where Self.Body == Never

A Simple Sample of NSViewControllerRepresentable

  1. Create a new SwiftUI app.
  2. Add a new file, choose Cocoa Class, name it as TextViewController.swift.
    Choose create xib file as well.
  3. Add a close button and label.

xib

  1. TextViewController.swift
//
//  TextViewController.swift
//  ViewControllerRepresentable Test
//
//  Created by zhaoxin on 2022/6/12.
//

import Cocoa

class TextViewController: NSViewController {
    @IBOutlet weak var label: NSTextField!
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    @IBAction func closeButtonClicked(_ sender: Any) {
        self.dismiss(sender)
    }
}
  1. TextView
//
//  TextView.swift
//  ViewControllerRepresentable Test
//
//  Created by zhaoxin on 2022/6/12.
//

import AppKit
import SwiftUI

struct TextView:NSViewControllerRepresentable {
    func makeNSViewController(context: Context) -> TextViewController {
        let controller  = TextViewController()        
        return controller
    }    
    
    func updateNSViewController(_ nsViewController: TextViewController, context: Context) {

    }
}
  1. ContentView.swift
//
//  ContentView.swift
//  ViewControllerRepresentable Test
//
//  Created by zhaoxin on 2022/6/12.
//

import SwiftUI

struct ContentView: View {
    @State var title = ""
    @State var showTextView = false
    
    var body: some View {
        HStack {
            TextField("Title", text: $title)
        
            Button("Show Text View") {
                showTextView = true
            }
            .sheet(isPresented: $showTextView) {
                TextView()
            }
        }
        .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

The app ran as below, but the close button didn't work.

first_sample

Coordinator

View Controllers are class, and View Representables are structs. We use Coordinator to pass changes to SwiftUI.

  1. TextView
//  ViewControllerRepresentable Test
//
//  Created by zhaoxin on 2022/6/12.
//

import AppKit
import SwiftUI

struct TextView:NSViewControllerRepresentable {
    @Environment(\.presentationMode) var presentationMode
    @Binding var title:String
    
    class Coordinator: NSObject, TextViewControllerDelegate {
        var parent: TextView

        init(_ parent: TextView) {
            self.parent = parent
        }
        
        func textView(_ tvc: TextViewController) {
            self.parent.presentationMode.wrappedValue.dismiss()
        }
    }
    
    func makeNSViewController(context: Context) -> TextViewController {
        let controller  = TextViewController()
        controller.delegate = context.coordinator
        
        return controller
    }
    
    func updateNSViewController(_ nsViewController: TextViewController, context: Context) {
        nsViewController.label.stringValue = title
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
}
  1. TextViewController
//
//  TextViewController.swift
//  ViewControllerRepresentable Test
//
//  Created by zhaoxin on 2022/6/12.
//

import Cocoa

class TextViewController: NSViewController {
    @IBOutlet weak var label: NSTextField!
    weak var delegate:TextViewControllerDelegate?
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    @IBAction func closeButtonClicked(_ sender: Any) {
        delegate?.textView(self)
    }
}

protocol TextViewControllerDelegate:AnyObject {
    func textView(_ textView:TextViewController)
}
  1. ContentView
//
//  ContentView.swift
//  ViewControllerRepresentable Test
//
//  Created by zhaoxin on 2022/6/12.
//

import SwiftUI

struct ContentView: View {
    @State var title = ""
    @State var showTextView = false
    
    var body: some View {
        HStack {
            TextField("Title", text: $title)
        
            Button("Show Text View") {
                showTextView = true
            }
            .sheet(isPresented: $showTextView) {
                TextView(title: $title)
            }
        }
        .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}//
//  ContentView.swift
//  ViewControllerRepresentable Test
//
//  Created by zhaoxin on 2022/6/12.
//

import SwiftUI

struct ContentView: View {
    @State var title = ""
    @State var showTextView = false
    
    var body: some View {
        HStack {
            TextField("Title", text: $title)
        
            Button("Show Text View") {
                showTextView = true
            }
            .sheet(isPresented: $showTextView) {
                TextView(title: $title)
            }
        }
        .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

App ran like this and the close button worked.

second_sample

Update Variables of NSViewController

You may aware that there was a func

func updateNSViewController(_ nsViewController: TextViewController, context: Context) {
    nsViewController.label.stringValue = title
}

We couldn't call nsViewController.label.stringValue = title in makeNSViewController, as in the make function the View Controller was created, but the xib was not loaded. We can think this update as viewDidLoad function in View Controller.

Resources

SwiftUI, Core Data and CloudKit Part 2

Core Data

According to Apple, adding CloudKit Core Data, was as easy as following three steps.

  1. Enable iCloud Capacity in Xcode and check CloudKit.
  2. Add a default iCloud container.
  3. Replacing NSPersistentContainer with NSPersistentCloudKitContainer.

Then everything will work.

Using Core Data With CloudKit

It is not true. I did that and that won't work. I even checked the Core Data model with "Used with CloudKit", and there was still an error and CloudKit schema couldn't be created as there was no store for CloudKit.

So I had to create a new project with Core Data and CloudKit enabled at the very beginning.

If you want to know how to convert a Core Data project with CloudKit. See this, Getting Started with Core Data and CloudKit

Create a new project

Create a new project named "Todo List 2" and enable Core Data and CloudKit.

Project Code

  1. Add iCloud capability and check CloudKit.
  2. Then add a default container "iCloud.(You app bundle id)"

iCloud Capability

This is enough for a macOS app. If yours is an iOS app, you must add Background Modes capability and check "Remote notifications".

Be careful when you testing your app. The development and release app used different databases in CloudKit. So if you test your app, you must use the development version.

References

SwiftUI, Core Data and CloudKit Part 1

Core Data

This is a simple project for SwiftUI, Core Data and CloudKit. In part 1, I will create a simple project in SwiftUI, then turn it into Core Data. In part 2, I will use CloudKit to sync with Core Data.

SwiftUI

Create a macOS SwiftUI project.

Project Code

//
//  ContentView.swift
//  Todo List
//
//  Created by zhaoxin on 2022/5/7.
//

import SwiftUI

struct ContentView: View {
    @State private var items = [Item]()
    @State private var addNewItem = false
    
    let addNewItemPublisher = NotificationCenter.default.publisher(for: AddItemView.addNewItem)
    
    var body: some View {
        VStack {
            ScrollView(.vertical, showsIndicators: true) {
                if items.isEmpty {
                    Text("No Data")
                } else {
                    List($items) { item in
                        HStack {
                            Text(DateFormatter.localizedString(from: item.startDate.wrappedValue, dateStyle: .none, timeStyle: .short))
                            Text(item.title.wrappedValue)
                        }
                    }
                    .frame(minWidth: 580, minHeight: 400, idealHeight: 600)
                }
            }
            .onReceive(addNewItemPublisher) { notification in
                if let userInfo = notification.userInfo as? [String:Item], let item = userInfo["new item"] {
                    items.append(item)
                    print(items)
                }
            }
            
            Button {
                add()
            } label: {
                Text("Add")
            }
            .sheet(isPresented: $addNewItem) {
                AddItemView()
            }
        }
        .padding()
        .frame(width: 600, height: 600, alignment: .center)
        
    }
    
    private func add() {
        addNewItem = true
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
//
//  AddItemView.swift
//  Todo List
//
//  Created by zhaoxin on 2022/5/7.
//

import SwiftUI

struct AddItemView: View {
    static let addNewItem = Notification.Name("addNewItem")
    
    @Environment(\.dismiss) private var dismiss
    @State private var item = Item(startDate: Date(), title: "")
    
    var body: some View {
        VStack {
            Text("Add New Item")
                .font(.title2)
            
            TextField("What to do?", text: $item.title, prompt: Text("Go shopping."))
            DatePicker("When?", selection: $item.startDate)
            
            HStack {
                Button {
                    save()
                } label: {
                    Text("Save")
                }
                .disabled(item.title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)

                Spacer()
                
                Button {
                    cancel()
                } label: {
                    Text("Cancel")
                }
            }
        }
        .padding()
    }
    
    private func save() {
        NotificationCenter.default.post(name: AddItemView.addNewItem, object: nil, userInfo: ["new item" : item])
        dismiss()
    }
    
    private func cancel() {
        dismiss()
    }
}

struct AddItemView_Previews: PreviewProvider {
    static var previews: some View {
        AddItemView()
    }
}
//
//  Model.swift
//  Todo List
//
//  Created by zhaoxin on 2022/5/7.
//

import Foundation

struct Item:Identifiable {
    let id = UUID().uuidString
    var startDate:Date
    var title:String
}

class ItemProvider:ObservableObject {
    @Published var items = [Item]()
}

You can test and run the app.

Core Data Basics

NSPersistentContainer

NSPersistentContainer handles the creation of the Core Data stack and offers access to the NSManagedObjectContext as well as a number of convenience methods.

NSManagedObjectModel

The NSManagedObjectModel instance describes the data that is going to be accessed by the Core Data stack. During the creation of the Core Data stack, the NSManagedObjectModel is loaded into memory as the first step in the creation of the stack.

NSPersistentStoreCoordinator

The NSPersistentStoreCoordinator sits in the middle of the Core Data stack. The coordinator is responsible for realizing instances of entities that are defined inside of the model. It creates new instances of the entities in the model, and it retrieves existing instances from a persistent store (NSPersistentStore).

NSManagedObjectContext

The managed object context (NSManagedObjectContext) is the object that your application will interact with the most, and therefore it is the one that is exposed to the rest of your application. Think of the managed object context as an intelligent scratch pad. When you fetch objects from a persistent store, you bring temporary copies onto the scratch pad where they form an object graph (or a collection of object graphs). You can then modify those objects however you like. Unless you actually save those changes, however, the persistent store remains unaltered.

All managed objects must be registered with a managed object context. You use the context to add objects to the object graph and remove objects from the object graph. The context tracks the changes you make, both to individual objects’ attributes and to the relationships between objects. By tracking changes, the context is able to provide undo and redo support for you. It also ensures that if you change relationships between objects, the integrity of the object graph is maintained.

If you choose to save the changes you have made, the context ensures that your objects are in a valid state. If they are, the changes are written to the persistent store (or stores), new records are added for objects you created, and records are removed for objects you deleted.

Without Core Data, you have to write methods to support archiving and unarchiving of data, to keep track of model objects, and to interact with an undo manager to support undo. In the Core Data framework, most of this functionality is provided for you automatically, primarily through the managed object context.

Core Data

Project Code

To use core data, first we must remove "Model.swift" file. Then we created a Core Data Model file, named it as "Model.xcdatamodeld".

Then we create it attributes.

core data model

Create ItemProvider

Add a new file, name it as "ItemProvider.swift".

//
//  ItemProvider.swift
//  Todo List
//
//  Created by zhaoxin on 2022/5/7.
//

import Foundation
import CoreData
import AppKit

class ItemProvider:ObservableObject {
    let container = NSPersistentContainer(name: "Model")
    
    init() {
        container.loadPersistentStores { description, error in
            if let error = error {
                let alert = NSAlert(error: error)
                NSSound.beep()
                alert.runModal()
            }
        }
    }
}

We must first create the NSPersistentContainer, or the app will not know how to create an instance of "Item".

Core Data in SwiftUI

We need to set managedObjectContext in environment, so it will be use in the SwiftUI app.

//
//  Todo_ListApp.swift
//  Todo List
//
//  Created by zhaoxin on 2022/5/7.
//

/*
 <a href="https://www.flaticon.com/free-icons/reminder" title="reminder icons">Reminder icons created by max.icons - Flaticon</a>
 */

import SwiftUI

@main
struct Todo_ListApp: App {
    @StateObject private var itemProvider = ItemProvider()
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.managedObjectContext, itemProvider.container.viewContext)
        }
    }
}

Then in ContentView.swift, we fetch the results from Core Data.

//
//  ContentView.swift
//  Todo List
//
//  Created by zhaoxin on 2022/5/7.
//

import SwiftUI

struct ContentView: View {
    @Environment(\.managedObjectContext) var managedObjectContext
    @FetchRequest(sortDescriptors: [SortDescriptor(\.startDate, order: .forward)]) var items:FetchedResults<Item>
    @State private var item:Item?
    
    var body: some View {
        VStack {
            ScrollView(.vertical, showsIndicators: true) {
                if items.isEmpty {
                    Text("No Data")
                } else {
                    List(items) { item in
                        HStack {
                            Text(DateFormatter.localizedString(from: item.startDate!, dateStyle: .none, timeStyle: .short))
                            Text(item.title!)
                        }
                    }
                    .frame(minWidth: 580, minHeight: 400, idealHeight: 600)
                }
            }
            
            Button {
                add()
            } label: {
                Text("Add")
            }
            .sheet(item: $item) { item in
                AddItemView(item: item)
            }
        }
        .padding()
        .frame(width: 600, height: 600, alignment: .center) 
    }
    
    private func add() {
        let item = Item(context: managedObjectContext)
        item.id = UUID()
        item.startDate = Date()
        self.item = item
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

In AddItemView, the similar.

//
//  AddItemView.swift
//  Todo List
//
//  Created by zhaoxin on 2022/5/7.
//

import SwiftUI
import CoreData

struct AddItemView: View {
    @Environment(\.managedObjectContext) var managedObjectContext
    @Environment(\.dismiss) private var dismiss
    @State var item:Item
    
    var body: some View {
        VStack {
            Text("Add New Item")
                .font(.title2)
            
            TextField("What to do?", text: Binding($item.title) ?? .constant(""), prompt: Text("Go shopping."))
            DatePicker("When?", selection: Binding($item.startDate) ?? .constant(Date()))
            
            HStack {
                Button {
                    save()
                } label: {
                    Text("Save")
                }

                Spacer()
                
                Button {
                    cancel()
                } label: {
                    Text("Cancel")
                }
            }
        }
        .padding()
    }
    
    private func save() {
        if var title = item.title {
            title = title.trimmingCharacters(in: .whitespacesAndNewlines)
            guard !title.isEmpty else {
                let alert = NSAlert()
                alert.messageText = NSLocalizedString("Title is empty!", comment: "")
                alert.alertStyle = .warning
                alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
                NSSound.beep()
                alert.runModal()
                
                return
            }
            
            do {
                item.title = title
                try managedObjectContext.save()
            } catch {
                let alert = NSAlert(error: error)
                NSSound.beep()
                alert.runModal()
            }
        }
        
        dismiss()
    }
    
    private func cancel() {
        managedObjectContext.rollback()
        dismiss()
    }
}

In SwiftUI, we use "NotificationCenter" to post a notification in save() function. In Core Data, we no longer need the notification as we could save directly and the change will automatically trigger the FetchedResults.

Reference

App Architecture of SwiftUI

SwiftUI

When developing with Cocoa/UIKit, the most common used architecture is MVC.

model_view_controller_2x

What is the architecture when using SwiftUI? Someone says it is MVVM. Someone says it is MV, MVC without C. I would like say it would be M-VC, Model with View and Controller.

M-VC

We want every element to be simple. For MV architecture, the View is too heavy for me. For M-VC, we want the View as simple as possible. The extra parts we think it to be Controller.

Split View into small Views

For a SwiftUI View, when one of the @State properties changed, all @State properties in the same view will be updated. If this is overhead, you can split those @State properties to another view, that will reduce CPU usage and speed up your app.