diff --git a/SwiftDemos/SwiftDemos/ContentView.swift b/SwiftDemos/SwiftDemos/ContentView.swift index e355961..12c44d5 100644 --- a/SwiftDemos/SwiftDemos/ContentView.swift +++ b/SwiftDemos/SwiftDemos/ContentView.swift @@ -8,7 +8,7 @@ import SwiftUI struct ContentView: View { - private var demos = ["COW"] + private var demos = ["COW", "PropertyWrapper"] var body: some View { NavigationStack { @@ -16,6 +16,8 @@ struct ContentView: View { ForEach(demos.enumerated(), id: \.element) { idx, item in if idx == 0 { NavigationLink(item, destination: COWView()) + } else if idx == 1 { + NavigationLink(item, destination: PropertyWrapperView()) } } } diff --git a/SwiftDemos/SwiftDemos/Features/PropertyWrapper.swift b/SwiftDemos/SwiftDemos/Features/PropertyWrapper.swift deleted file mode 100644 index b35d364..0000000 --- a/SwiftDemos/SwiftDemos/Features/PropertyWrapper.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// PropertyWrapper.swift -// SwiftDemos -// -// Created by Jinchao Lin on 2026/2/25. -// - -import Foundation diff --git a/SwiftDemos/SwiftDemos/Features/TypedThrows.swift b/SwiftDemos/SwiftDemos/Features/TypedThrows.swift index 75f37d3..bce1420 100644 --- a/SwiftDemos/SwiftDemos/Features/TypedThrows.swift +++ b/SwiftDemos/SwiftDemos/Features/TypedThrows.swift @@ -20,7 +20,7 @@ enum FileError: Error { // throws:声明函数可能抛出错误 func fetchData(from urlString: String) throws -> Data { - guard let url = URL(string: urlString) else { + guard let _ = URL(string: urlString) else { throw NetworkError.invalidURL } // ... @@ -55,7 +55,7 @@ func fetch() throws(NetworkError) -> Data { func test2() { // 调用方 catch 分支无需类型转换 do { - let data = try fetch() + let _ = try fetch() } catch { // error 的类型是 NetworkError(不是 any Error) switch error { diff --git a/SwiftDemos/SwiftDemos/PropertyWrapper/PropertyWrapper.swift b/SwiftDemos/SwiftDemos/PropertyWrapper/PropertyWrapper.swift new file mode 100644 index 0000000..2af8485 --- /dev/null +++ b/SwiftDemos/SwiftDemos/PropertyWrapper/PropertyWrapper.swift @@ -0,0 +1,57 @@ +// +// PropertyWrapper.swift +// SwiftDemos +// +// Created by Jinchao Lin on 2026/2/25. +// + +import Foundation +import Combine + +@propertyWrapper +struct UserDefault { + let key: String + let defaultValue: T + + var wrappedValue: T { + get { + UserDefaults.standard.object(forKey: key) as? T ?? defaultValue + } + nonmutating set { + UserDefaults.standard.set(newValue, forKey: key) + } + } + + // 通过 $ 访问 + var projectedValue: AnyPublisher { + /// publisher(for:) 是 Combine 的 KVO Publisher,\.self 表示观察 UserDefaults 对象本身。 + /// 但 UserDefaults.standard 是单例,对象引用永远不变,KVO 只在订阅时触发一次初始值,之后不再触发 +// UserDefaults.standard +// .publisher(for: \.self) // 监听整个 UserDefaults +// .compactMap { $0.object(forKey: self.key) as? T } +// .eraseToAnyPublisher() + + NotificationCenter.default + .publisher(for: UserDefaults.didChangeNotification) + .map { _ in UserDefaults.standard.object(forKey: key) as? T ?? defaultValue } + .prepend(wrappedValue) // 补一个初始值,行为与之前一致 + .eraseToAnyPublisher() + } +} + +/* + // 源码 + struct Foo { + @UserDefault(key: "x", defaultValue: 0) + var x: Int + } + + // 编译器生成(等价代码) + struct Foo { + private var _x = UserDefault(key: "x", defaultValue: 0) + var x: Int { + get { _x.wrappedValue } + set { _x.wrappedValue = newValue } + } + } + */ diff --git a/SwiftDemos/SwiftDemos/PropertyWrapper/PropertyWrapperView.swift b/SwiftDemos/SwiftDemos/PropertyWrapper/PropertyWrapperView.swift new file mode 100644 index 0000000..bb76119 --- /dev/null +++ b/SwiftDemos/SwiftDemos/PropertyWrapper/PropertyWrapperView.swift @@ -0,0 +1,35 @@ +// +// PropertyWrapperView.swift +// SwiftDemos +// +// Created by Jinchao Lin on 2026/2/26. +// + +import SwiftUI + +struct PropertyWrapperView: View { + @UserDefault(key: "isDarkMode", defaultValue: false) + var isDarkMode: Bool // _isDarkMode.wrappedValue + + var body: some View { + VStack { + Button("Switch to Dark") { + isDarkMode = true + } + .padding() + + Button("Switch to White") { + isDarkMode = false + } + .padding() + } + .navigationTitle("Property Wrapper") + .onReceive($isDarkMode) { newValue in + debugPrint("isDarkMode changed to \(newValue)") + } + } +} + +#Preview { + PropertyWrapperView() +}