SwiftUI 2.0 — Apps, Scenes, and New Code Structures (Part 2)

Published on

Get weekly handpicked updates on Swift and SwiftUI!

In the previous article, we briefly explored Apps, Scenes, and the application of several built-in Scenes. In this article, we will focus on how to more efficiently organize Data Flow under the new code structure of SwiftUI 2.0.

New Features

@AppStorage

AppStorage is a property wrapper provided by Apple for manipulating UserDefaults. This feature has been developed by many developers since Swift introduced the propertyWrapper characteristic. There’s nothing special about its functionality, but its name corresponds to the new App protocol, making it easier to understand its applicable lifecycle.

  • Data is persistent; it remains even after the app is closed
  • It only wraps UserDefaults, so data can be read normally through UserDefaults
  • Supports the same data types as UserDefaults; not suitable for complex data types
  • Applicable at any View level in the app, but doesn’t work (without errors) at the app level
Swift
@main
struct AppStorageTest: App {
    // No errors, but doesn't work
    //@AppStorage("count") var count = 0
    var body: some Scene {
        WindowGroup {
            RootView()
            CountView()
        }
    }
}

struct RootView: View {
    @AppStorage("count") var count = 0
    var body: some View {
        List{
            Button("+1"){
                count += 1
            }
        }
    }
}

struct CountView:View{
    @AppStorage("count") var count = 0
    var body: some View{
        Text("Count:\(count)")
    }
}

@SceneStorage

The usage of @SceneStorage is very similar to @AppStorage, but its scope is limited to the current Scene.

  • The data scope is limited within the Scene
  • The lifecycle is consistent with the Scene; for example, on iPadOS, if you force quit a split-screen app, the system may retain the last Scene information when opening the app next time. However, if you quit a Scene individually, the data is lost
  • Supports types similar to @AppStorage, suitable for lightweight data
  • Suitable for saving Scene-specific information, like TabView selections, independent layouts, etc.
Swift
@main
struct NewAllApp: App {
    var body: some Scene {
        WindowGroup{
            ContentView1()
        }
    }
}

struct ContentView:View{
    @SceneStorage("tabSeleted") var selection = 2
    var body:some View{
        TabView(selection:$selection){
            Text("1").tabItem { Text("1") }.tag(1)
            Text("2").tabItem { Text("2") }.tag(2)
            Text("3").tabItem { Text("3") }.tag(3)
        }
    }
}

abc

Note: The above code runs normally on PadOS, but crashes on macOS. This is likely a bug.

Data Flow

Methods

In SwiftUI 2.0, Apple added new property wrappers like @AppStorage, @SceneStorage, @StateObject, etc. Based on my understanding, I have summarized some of the property wrappers provided by SwiftUI as follows:

propertyWrapperSheet

With these upgrades, SwiftUI has significantly improved the lifecycle management of data at various levels, providing solutions for different types, situations, and purposes, facilitating the creation of Data Flow in line with SwiftUI. We can choose the suitable Source of truth method according to our needs.

To learn more details, you can refer to my other articles:

@State Research in SwiftUI

Research on @StateObject

ObservableObject Research - Expressing Love is Not Easy

Changes

In SwiftUI 1.0, we usually create data with a lifecycle consistent with the app in AppDelegate (like CoreData’s Container), and create data sources like Store in SceneDelegate, injecting them through .environmentObject. However, with the changes in the program entry point in SwiftUI 2.0 and the adoption of a new Delegate response method, we can now complete the above tasks with more concise and clear code.

Swift
@main
struct NewAllApp: App {
    @StateObject var store = Store()
    var body: some Scene {
        WindowGroup{
            ContentView()
                .environmentObject(store

)
        }
    }
}

class Store:ObservableObject{
    @Published var count = 0
}

In the above example, replacing

Swift
@StateObject var store = Store()

with

Swift
let store = Store()

currently has the same effect.

Although SceneBuilder and CommandBuilder currently do not support dynamic updates and logical judgments, I believe that we might be able to use similar code to perform many interesting tasks in the near future. The current code cannot be executed.

Swift
@main
struct NewAllApp: App {
    @StateObject var store = Store()
    @SceneBuilder var body: some Scene {
        //@SceneBuilder currently does not support judgment, but it should in the future
        if store.scene == 0 {
        WindowGroup{
            ContentView1()
                .environmentObject(store)
        }
        .onChange(of: store.number){ value in
            print(value)
        }
        .commands{
            CommandMenu("DynamicButton"){
                //Currently unable to switch content dynamically, suspected bug, feedback submitted
                switch store.number{
                case 0:
                    Button("0"){}
                case 1:
                    Button("1"){}
                default:
                    Button("other"){}
                }
            }
        }
        else {
         DocumentGroup(newDocment:TextFile()){ file in
              TextEditorView(document:file.$document)
         }
        }
        
        Settings{
            VStack{
               //Can switch normally
                Text("\(store.number)")
                    .padding(.all, 50)
            }
        }

    }
}

struct ContentView1:View{
    @EnvironmentObject var store:Store
    var body:some View{
        VStack{
        Picker("select",selection:$store.number){
            Text("0").tag(0)
            Text("1").tag(1)
            Text("2").tag(2)
        }
        .pickerStyle(SegmentedPickerStyle())
        .padding()
        }
    }
}

class Store:ObservableObject{
    @Published var number = 0
    @Published var scene = 0
}

Cross-Platform Code

In the previous article, we introduced the new @UIApplicationDelegateAdaptor usage method. We can also directly create a store that supports Delegate.

Swift
import SwiftUI

class Store:NSObject,ObservableObject{
    @Published var count = 0
}

#if os(iOS)
extension Store:UIApplicationDelegate{
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        print("launch")
        return true
    }
}
#endif

@main
struct AllInOneApp: App {
    #if os(iOS)
    @UIApplicationDelegateAdaptor(Store.self) var store
    #else
    @StateObject var store = Store()
    #endif
    
    @Environment(\.scenePhase) var phase

    @SceneBuilder var body: some Scene {
            WindowGroup {
                RootView()
                    .environmentObject(store)
            }
            .onChange(of: phase){phase in
                switch phase{
                case .active:
                    print("active")
                case .inactive:
                    print("inactive")
                case .background:
                    print("background")
                @unknown default:
                    print("for future")
                }

            }
      
        #if os(macOS)
        Settings{
            Text("Preferences").padding(.all, 50)
        }
        #endif
    }
}

Conclusion

In ObservableObject Research - Expressing Love is Not Easy, we discussed that SwiftUI prefers us not to create a heavy Single source of truth. Instead, treat each functional module as an independent state machine (together forming a large stateful app), using methods that are more precisely controllable over the lifecycle and scope to create regional sources of truth.

From the content upgraded since the first version of SwiftUI, it still follows this line of thought.

Weekly Swift & SwiftUI insights, delivered every Monday night. Join developers worldwide.
Easy unsubscribe, zero spam guaranteed