{"id":6927,"date":"2025-11-30T16:46:28","date_gmt":"2025-11-30T08:46:28","guid":{"rendered":"https:\/\/t.n-years.com\/?p=6927"},"modified":"2025-11-30T16:46:28","modified_gmt":"2025-11-30T08:46:28","slug":"swiftui-%e4%b8%ad%e7%9a%84-%e5%b1%9e%e6%80%a7%e5%8c%85%e8%a3%85%e5%99%a8%e7%a4%ba%e4%be%8b","status":"publish","type":"post","link":"https:\/\/t.n-years.com\/?p=6927","title":{"rendered":"SwiftUI \u4e2d\u7684 \u5c5e\u6027\u5305\u88c5\u5668\u793a\u4f8b"},"content":{"rendered":"<p>\u8fd9\u662f\u4e00\u4e2a\u5c06 <strong><code>@State<\/code><\/strong>, <strong><code>@Binding<\/code><\/strong>, <strong><code>@StateObject<\/code><\/strong>, <strong><code>@ObservedObject<\/code><\/strong>, <strong><code>@EnvironmentObject<\/code><\/strong>, \u548c <strong><code>@AppStorage<\/code><\/strong> \u5168\u90e8\u4e32\u8054\u8d77\u6765\u7684\u5b8c\u6574\u793a\u4f8b\u3002<\/p>\n<h3>\u573a\u666f\u6a21\u62df\uff1a\u4e00\u4e2a\u7b80\u5355\u7684\u201c\u7528\u6237\u4eea\u8868\u76d8\u201d App<\/h3>\n<p>\u5728\u8fd9\u4e2a App \u4e2d\uff1a<\/p>\n<ol>\n<li><strong>\u5168\u5c40\u73af\u5883<\/strong>\uff1a\u6709\u4e00\u4e2a <code>UserSession<\/code>\uff08\u7528\u6237\u767b\u5f55\u72b6\u6001\uff09\uff0c\u5168 App \u5171\u4eab\u3002<\/li>\n<li><strong>\u672c\u5730\u8bbe\u7f6e<\/strong>\uff1a\u6709\u4e00\u4e2a\u201c\u901a\u77e5\u5f00\u5173\u201d\uff0c\u76f4\u63a5\u5b58\u5165 UserDefaults\u3002<\/li>\n<li><strong>\u72ec\u7acb\u9875\u9762\u903b\u8f91<\/strong>\uff1a\u6709\u4e00\u4e2a\u8ba1\u65f6\u5668\u9875\u9762\uff0c\u62e5\u6709\u81ea\u5df1\u7684 ViewModel\u3002<\/li>\n<li><strong>\u7236\u5b50\u4f20\u503c<\/strong>\uff1a\u7236\u9875\u9762\u63a7\u5236\u5f39\u7a97\uff0c\u5b50\u9875\u9762\u4fee\u6539\u7236\u9875\u9762\u7684\u6807\u9898\u3002<\/li>\n<\/ol>\n<hr \/>\n<h3>1. \u5b9a\u4e49\u6570\u636e\u6a21\u578b (Model &amp; ViewModel)<\/h3>\n<p>\u9996\u5148\u5b9a\u4e49\u4e24\u4e2a\u7c7b\uff0c\u4e00\u4e2a\u7528\u4e8e\u5168\u5c40\u5171\u4eab\uff0c\u4e00\u4e2a\u7528\u4e8e\u7279\u5b9a\u9875\u9762\u3002<\/p>\n<pre><code class=\"language-swift\">import SwiftUI\nimport Combine\n\n\/\/ 1. \u5168\u5c40\u5171\u4eab\u5bf9\u8c61 (\u7c7b\u4f3c\u4e8e\u4f60\u7684 SyncSettingsViewModel)\n\/\/ \u573a\u666f\uff1a\u9700\u8981\u5728\u4efb\u4f55\u9875\u9762\u90fd\u80fd\u8bbf\u95ee\u5230\u7684\u6570\u636e\nclass UserSession: ObservableObject {\n    @Published var username: String = &quot;Guest&quot;\n    @Published var isLoggedIn: Bool = false\n}\n\n\/\/ 2. \u5c40\u90e8\u9875\u9762\u4e13\u7528\u7684 ViewModel\n\/\/ \u573a\u666f\uff1a\u53ea\u670d\u52a1\u4e8e\u201c\u8ba1\u65f6\u5668\u201d\u8fd9\u4e2a\u529f\u80fd\nclass TimerViewModel: ObservableObject {\n    @Published var seconds: Int = 0\n    private var timer: AnyCancellable?\n\n    func start() {\n        if timer != nil { return }\n        timer = Timer.publish(every: 1, on: .main, in: .common)\n            .autoconnect()\n            .sink { _ in self.seconds += 1 }\n    }\n\n    func stop() {\n        timer?.cancel()\n        timer = nil\n    }\n}<\/code><\/pre>\n<hr \/>\n<h3>2. \u89c6\u56fe\u5c42\u7ea7 (Views)<\/h3>\n<p>\u8bf7\u4ed4\u7ec6\u770b\u6ce8\u91ca\uff0c\u6bcf\u4e00\u5904\u6ce8\u89e3\u7684\u9009\u62e9\u90fd\u6709\u5176\u7279\u5b9a\u7684\u7406\u7531\u3002<\/p>\n<pre><code class=\"language-swift\">\/\/ \u5165\u53e3\uff1a\u6ce8\u5165\u73af\u5883\u5bf9\u8c61\nstruct MyApp: App {\n    \/\/ \u521b\u5efa\u552f\u4e00\u7684\u5168\u5c40\u5b9e\u4f8b\n    @StateObject private var userSession = UserSession()\n\n    var body: some Scene {\n        WindowGroup {\n            DashboardView()\n                .environmentObject(userSession) \/\/ \u6ce8\u5165\u73af\u5883\uff0c\u4e4b\u540e\u6240\u6709\u5b50 View \u90fd\u80fd\u7528\n        }\n    }\n}\n\n\/\/ --- \u4e3b\u89c6\u56fe ---\nstruct DashboardView: View {\n    \/\/ A. @EnvironmentObject: \u4ece\u201c\u7a7a\u6c14\u201d\u4e2d\u6293\u53d6\u521a\u624d\u6ce8\u5165\u7684 session\n    @EnvironmentObject var session: UserSession\n\n    \/\/ B. @AppStorage: \u81ea\u52a8\u8bfb\u5199 UserDefaults\uff0c\u6301\u4e45\u5316\u5b58\u50a8\n    @AppStorage(&quot;enableNotifications&quot;) var enableNotifs: Bool = true\n\n    \/\/ C. @State: \u7ba1\u7406\u5f53\u524d View \u7684 UI \u72b6\u6001 (\u662f\u5426\u663e\u793a\u7f16\u8f91\u5f39\u7a97)\n    @State private var showEditSheet = false\n\n    var body: some View {\n        NavigationView {\n            VStack(spacing: 20) {\n                Text(&quot;\u6b22\u8fce, \\(session.username)&quot;)\n                    .font(.largeTitle)\n\n                \/\/ \u4f7f\u7528 AppStorage \u7684\u5f00\u5173\uff0c\u91cd\u542f App \u540e\u72b6\u6001\u4f9d\u7136\u4fdd\u7559\n                Toggle(&quot;\u63a5\u6536\u901a\u77e5&quot;, isOn: $enableNotifs)\n                    .padding()\n\n                NavigationLink(&quot;\u8fdb\u5165\u8ba1\u65f6\u5668\u9875\u9762&quot;) {\n                    \/\/ \u8df3\u8f6c\u5230\u5b50\u9875\u9762\n                    TimerPageView()\n                }\n\n                Button(&quot;\u4fee\u6539\u7528\u6237\u540d&quot;) {\n                    showEditSheet = true\n                }\n            }\n            .padding()\n            .sheet(isPresented: $showEditSheet) {\n                \/\/ \u4f20\u9012 Binding \u7ed9\u5b50\u89c6\u56fe\n                EditNameView(name: $session.username)\n            }\n        }\n    }\n}\n\n\/\/ --- \u5b50\u89c6\u56fe 1\uff1a\u62e5\u6709\u72ec\u7acb\u903b\u8f91\u7684\u9875\u9762 ---\nstruct TimerPageView: View {\n    \/\/ D. @StateObject: \u6211\u662f\u8fd9\u4e2a ViewModel \u7684\u201c\u4e3b\u4eba\u201d\n    \/\/ \u8fd9\u4e2a View \u521d\u59cb\u5316\u65f6\uff0c\u521b\u5efa ViewModel\u3002View \u5237\u65b0\u65f6\uff0c\u5b83\u4e0d\u4f1a\u6b7b\u3002\n    @StateObject private var timerVM = TimerViewModel()\n\n    var body: some View {\n        VStack {\n            Text(&quot;\u8ba1\u65f6: \\(timerVM.seconds) \u79d2&quot;)\n                .font(.title)\n\n            HStack {\n                Button(&quot;\u5f00\u59cb&quot;) { timerVM.start() }\n                Button(&quot;\u505c\u6b62&quot;) { timerVM.stop() }\n            }\n\n            Divider()\n\n            \/\/ \u628a VM \u4f20\u7ed9\u66f4\u5c0f\u7684\u5b50\u7ec4\u4ef6\n            TimerDisplayView(vm: timerVM)\n        }\n    }\n}\n\n\/\/ --- \u5b50\u89c6\u56fe 2\uff1a\u590d\u7528\u7ec4\u4ef6 ---\nstruct TimerDisplayView: View {\n    \/\/ E. @ObservedObject: \u6211\u53ea\u662f\u201c\u89c2\u5bdf\u8005\u201d\n    \/\/ \u6211\u4e0d\u521b\u5efa\u5b83\uff0c\u6211\u53ea\u63a5\u6536\u7236\u89c6\u56fe\u4f20\u4e0b\u6765\u7684\u5b9e\u4f8b\u3002\n    @ObservedObject var vm: TimerViewModel\n\n    var body: some View {\n        Text(&quot;\u5b50\u7ec4\u4ef6\u4e5f\u5728\u76d1\u542c: \\(vm.seconds)&quot;)\n            .foregroundColor(.gray)\n            .font(.caption)\n    }\n}\n\n\/\/ --- \u5b50\u89c6\u56fe 3\uff1a\u4fee\u6539\u5f39\u7a97 ---\nstruct EditNameView: View {\n    \/\/ F. @Binding: \u201c\u4f20\u58f0\u7b52\u201d\n    \/\/ \u6211\u4e0d\u5b58\u6570\u636e\uff0c\u6211\u4fee\u6539\u7684\u662f DashboardView \u91cc\u7684 session.username\n    @Binding var name: String\n    @Environment(\\.dismiss) var dismiss \/\/ \u7cfb\u7edf\u73af\u5883\u503c\uff0c\u7528\u4e8e\u5173\u95ed\u5f39\u7a97\n\n    var body: some View {\n        VStack {\n            TextField(&quot;\u8f93\u5165\u65b0\u540d\u5b57&quot;, text: $name)\n                .textFieldStyle(RoundedBorderTextFieldStyle())\n                .padding()\n\n            Button(&quot;\u5b8c\u6210&quot;) {\n                dismiss()\n            }\n        }\n        .padding()\n    }\n}<\/code><\/pre>\n<hr \/>\n<h3>3. \u4ee3\u7801\u903b\u8f91\u5927\u590d\u76d8<\/h3>\n<table>\n<thead>\n<tr>\n<th style=\"text-align: left;\">\u4ee3\u7801\u4f4d\u7f6e<\/th>\n<th style=\"text-align: left;\">\u4f7f\u7528\u7684\u6ce8\u89e3<\/th>\n<th style=\"text-align: left;\">\u4e3a\u4ec0\u4e48\u9009\u5b83\uff1f<\/th>\n<th style=\"text-align: left;\">\u8fd9\u91cc\u7684\u884c\u4e3a<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"text-align: left;\"><strong>MyApp<\/strong><\/td>\n<td style=\"text-align: left;\"><code>@StateObject<\/code><\/td>\n<td style=\"text-align: left;\">\u521b\u5efa\u5168\u5c40\u552f\u4e00\u7684 <code>UserSession<\/code> \u6e90\u5934\u3002<\/td>\n<td style=\"text-align: left;\">\u4fdd\u8bc1 App \u751f\u547d\u5468\u671f\u5185 session \u6d3b\u7740\u3002<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left;\"><strong>DashboardView<\/strong><\/td>\n<td style=\"text-align: left;\"><code>@EnvironmentObject<\/code><\/td>\n<td style=\"text-align: left;\"><code>UserSession<\/code> \u5df2\u7ecf\u5728 MyApp \u6ce8\u5165\u4e86\uff0c\u8fd9\u91cc\u76f4\u63a5\u53d6\u3002<\/td>\n<td style=\"text-align: left;\">\u5982\u679c\u5728\u5176\u4ed6\u9875\u9762\u6539\u4e86\u540d\u5b57\uff0c\u8fd9\u91cc\u4f1a\u81ea\u52a8\u66f4\u65b0\u3002<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left;\"><strong>DashboardView<\/strong><\/td>\n<td style=\"text-align: left;\"><code>@AppStorage<\/code><\/td>\n<td style=\"text-align: left;\">\u9700\u8981\u8bb0\u4f4f\u7528\u6237\u7684\u201c\u901a\u77e5\u8bbe\u7f6e\u201d\uff0c\u5373\u4f7f\u6740\u8fdb\u7a0b\u4e5f\u8981\u8bb0\u4f4f\u3002<\/td>\n<td style=\"text-align: left;\">\u81ea\u52a8\u5b58\u53d6 UserDefaults \u4e2d\u7684 &quot;enableNotifications&quot;\u3002<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left;\"><strong>DashboardView<\/strong><\/td>\n<td style=\"text-align: left;\"><code>@State<\/code><\/td>\n<td style=\"text-align: left;\">\u63a7\u5236\u5f39\u7a97 (<code>showEditSheet<\/code>) \u662f\u7eaf UI \u903b\u8f91\uff0c\u53ea\u5c5e\u4e8e\u5f53\u524d\u9875\u3002<\/td>\n<td style=\"text-align: left;\">\u53ea\u6709\u8fd9\u4e2a View \u5173\u5fc3\u5f39\u7a97\u662f\u5f00\u662f\u5173\u3002<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left;\"><strong>EditNameView<\/strong><\/td>\n<td style=\"text-align: left;\"><code>@Binding<\/code><\/td>\n<td style=\"text-align: left;\">\u5f39\u7a97\u9700\u8981\u4fee\u6539\u7236\u89c6\u56fe\u7684\u6570\u636e\uff0c\u4f46\u4e0d\u9700\u8981\u62e5\u6709\u6570\u636e\u3002<\/td>\n<td style=\"text-align: left;\">\u5b50\u89c6\u56fe\u8f93\u5165\u6846\u53d8\u52a8 -> \u7236\u89c6\u56fe\u6570\u636e\u53d8\u52a8\u3002<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left;\"><strong>TimerPageView<\/strong><\/td>\n<td style=\"text-align: left;\"><code>@StateObject<\/code><\/td>\n<td style=\"text-align: left;\">\u8fd9\u4e2a\u9875\u9762<strong>\u8d1f\u8d23\u521b\u5efa<\/strong>\u8ba1\u65f6\u5668\u903b\u8f91 (<code>TimerViewModel<\/code>)\u3002<\/td>\n<td style=\"text-align: left;\">\u9875\u9762\u5b58\u5728\u65f6\uff0c\u8ba1\u65f6\u5668\u5c31\u5b58\u5728\uff1b\u9875\u9762\u9500\u6bc1\uff0c\u8ba1\u65f6\u5668\u9500\u6bc1\u3002<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left;\"><strong>TimerDisplayView<\/strong><\/td>\n<td style=\"text-align: left;\"><code>@ObservedObject<\/code><\/td>\n<td style=\"text-align: left;\">\u5b83\u53ea\u8d1f\u8d23\u663e\u793a\uff0c<strong>\u501f\u7528<\/strong>\u7236\u89c6\u56fe\u521b\u5efa\u597d\u7684 ViewModel\u3002<\/td>\n<td style=\"text-align: left;\">\u8ddf\u7740 <code>TimerViewModel<\/code> \u7684\u53d8\u5316\u5237\u65b0 UI\u3002<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h3>\u4e0b\u4e00\u6b65\u5efa\u8bae<\/h3>\n<p>\u8fd9\u4e2a\u793a\u4f8b\u51e0\u4e4e\u6db5\u76d6\u4e86 95% \u7684 SwiftUI \u5f00\u53d1\u573a\u666f\u3002<\/p>\n<p>\u5982\u679c\u4f60\u7684\u9879\u76ee\u662f\u7528 <strong>iOS 17<\/strong> \u5f00\u53d1\u7684\uff0c\u4f60\u8fd8\u53ef\u4ee5\u4f7f\u7528\u66f4\u73b0\u4ee3\u7684 <strong>Observation \u6846\u67b6<\/strong>\uff08<code>@Observable<\/code> \u5b8f\uff09\uff0c\u5b83\u628a <code>@StateObject<\/code>\u3001<code>@ObservedObject<\/code> \u548c <code>@EnvironmentObject<\/code> \u7684\u5199\u6cd5\u5168\u90e8\u7edf\u4e00\u7b80\u5316\u4e86\u3002<\/p>\n<p><strong>\u4f60\u60f3\u770b\u770b\u5982\u679c\u7528 iOS 17 \u7684\u65b0\u5199\u6cd5\uff0c\u8fd9\u6bb5\u4ee3\u7801\u4f1a\u7b80\u5316\u6210\u4ec0\u4e48\u6837\u5417\uff1f<\/strong><\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u8fd9\u662f\u4e00\u4e2a\u5c06 @State, @Binding, @StateObject, @ObservedObject, &#8230; <\/p>\n<div class=\"read-more navbutton\"><a href=\"https:\/\/t.n-years.com\/?p=6927\">\u9605\u8bfb\u66f4\u591a<i class=\"fa fa-angle-double-right\"><\/i><\/a><\/div>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[135],"tags":[],"class_list":["post-6927","post","type-post","status-publish","format-standard","hentry","category-135"],"_links":{"self":[{"href":"https:\/\/t.n-years.com\/index.php?rest_route=\/wp\/v2\/posts\/6927","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/t.n-years.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/t.n-years.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/t.n-years.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/t.n-years.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=6927"}],"version-history":[{"count":1,"href":"https:\/\/t.n-years.com\/index.php?rest_route=\/wp\/v2\/posts\/6927\/revisions"}],"predecessor-version":[{"id":6928,"href":"https:\/\/t.n-years.com\/index.php?rest_route=\/wp\/v2\/posts\/6927\/revisions\/6928"}],"wp:attachment":[{"href":"https:\/\/t.n-years.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=6927"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/t.n-years.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=6927"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/t.n-years.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=6927"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}