# #3 OOTD Store - [medium 連結](https://medium.com/@tonysu1204/3-ootd-store-14267be87be2) - [app 操作畫面 - 橫擺](https://www.youtube.com/watch?v=IP5bKzs6GMc) - [app 操作畫面 - 直擺](https://www.youtube.com/watch?v=HQTdsDxuSy0) - [github 連結](https://github.com/Ateto1204/OOTD-Store-iOS) - [app 畫面截圖](https://drive.google.com/drive/folders/1A9uzrbva26cLYQKpgYHT7kSO8Hta3955?usp=sharing) ## 重點程式碼講解 ### 使用 SwiftUI 的 UI 元件 - Text、Image、Button、TextField、Picker :::spoiler 程式碼 ```swift= // 登入系統介面 struct loginView: View { public let status: String @Binding public var login: Bool @State private var buttonPressed = false @State private var user = "" @State private var pwd = "" @State private var alertMsg = "null" var body: some View { ZStack { Color(.white) VStack { VStack(spacing: 20) { Text(status) .font(.title) .bold() .foregroundColor(.black) TextField(text: $user) { Text("帳號") .foregroundColor(.gray) } .foregroundColor(.black) TextField(text: $pwd) { Text("密碼") .foregroundColor(.gray) } .foregroundColor(.black) } .padding(EdgeInsets(top: 50, leading: 0, bottom: 0, trailing: 0)) Spacer() Button { checkCredentials() buttonPressed = true } label: { ZStack { RoundedRectangle(cornerRadius: 12) .frame(width: 100, height: 50) Text("完成") .foregroundColor(.white) } } .padding(EdgeInsets(top: 0, leading: 0, bottom: 50, trailing: 0)) } .padding(EdgeInsets(top: 0, leading: 25, bottom: 0, trailing: 25)) .alert(alertMsg, isPresented: $buttonPressed) { Button ("OK") { buttonPressed = false } } } } // 檢查帳號密碼是否合法以及登入是否成功 private func checkCredentials() { // 檢查帳號或密碼欄位是否為空白 guard !user.isEmpty && !pwd.isEmpty else { alertMsg = "帳號或密碼不能為空白" return } if status == "登入" { for item in MyView.accounts.indices { if MyView.accounts[item].username == user { if MyView.accounts[item].password == pwd { alertMsg = "登入成功" MyView.username = user login = true } else { alertMsg = "密碼不正確" } break } else { alertMsg = "帳號不存在" } } } else { alertMsg = "註冊成功" MyView.accounts.append(account(username: user, password: pwd, accumulation: 0)) MyView.username = user login = true } } } ``` ::: ### 使用 @State 和 @Binding :::spoiler 程式碼 ```swift= // 登入系統介面登入按鈕 struct ButtonView: View { @Binding public var login: Bool @State private var buttonPressed = false public var status: String var body: some View { Button { buttonPressed = true } label: { ZStack { RoundedRectangle(cornerRadius: 10) .frame(width: 70, height: 40) Text(status) .foregroundColor(.white) } } .sheet(isPresented: $buttonPressed) { loginView(status: status, login: $login) } } } ``` ::: ### 使用 Alert :::spoiler 程式碼 ```swift= // 顯示登入系統畫面及使用者帳戶畫面 struct MyView: View { @State private var login = false @State private var showLoginView = false @State private var status = "登入" @State private var showAlert = false public static var username = "unknown" public static var accounts = [account.demoAccount] var body: some View { NavigationStack { ZStack { Color(.white) if !login { // 使用者登入前的介面 VStack (spacing: 40) { Text("請先登入") .font(.title) .bold() .foregroundColor(.black) HStack (spacing: 20) { ButtonView(login: $login, status: "註冊") ButtonView(login: $login, status: "登入") } } .background(Color(.white)) } else { // 使用者登入後的介面 VStack (alignment: .leading, spacing: 40) { Text("我的帳號") .foregroundColor(Color(.black)) .font(.title) .bold() .padding(EdgeInsets(top: 25, leading: 25, bottom: 0, trailing: 200)) List { HStack (spacing: 0) { Group { Text("我的帳戶:") Text(MyView.username) } .foregroundColor(.black) .listRowBackground(Color(.white)) } .listRowBackground(Color(.white)) // 導向使用者購物車介面 NavigationLink { cartView() } label: { Text("我的購物車") .foregroundColor(.black) } .listRowBackground(Color(.white)) // 登出當前帳戶按鈕 Button { showAlert = true login = false } label: { Text("登出") .foregroundColor(.black) } .listRowBackground(Color.white) } .listStyle(PlainListStyle()) .listRowBackground(Color(.white)) } .background(Color(.white)) } } .background(Color(.white)) .alert("成功登出", isPresented: $showAlert) { Button ("OK") { } } } } } ``` ::: ### 使用 Sheet 或 FullscreenCover 切換頁面 :::spoiler 程式碼 ```swift= // 登入系統介面登入按鈕 struct ButtonView: View { @Binding public var login: Bool @State private var buttonPressed = false public var status: String var body: some View { Button { buttonPressed = true } label: { ZStack { RoundedRectangle(cornerRadius: 10) .frame(width: 70, height: 40) Text(status) .foregroundColor(.white) } } .sheet(isPresented: $buttonPressed) { loginView(status: status, login: $login) } } } ``` ::: ## 重點程式碼講解 ### 開頭動畫 - 利用多層 opacity 特效來顯示啟動動畫 - `Welcome` 字樣 - 整個 `launchView` :::spoiler 程式碼 ```swift= // APP啟動畫面 struct launchView: View { @State private var opacity: Double = 0 var body: some View { ZStack { Color(.white) Text("- Welcome -") .font(.title) .bold() .foregroundColor(.black) .opacity(opacity) .animation(.easeInOut(duration: 4), value: opacity) .onAppear { opacity = 10 } } .background(Color(.white)) } } ``` ::: ### 熱門商品顯示介面 - 商品列表可鉛直及水平滾動 - 優秀的使用者介面 - 極大量的商品資料 - 至少 20 項商品項目 - 包含超過 200 張 Images :::spoiler 程式碼 ```swift= // 以左右滾動顯示同類別商品 struct goodsRow: View { let title: String let goods: [Good] var body: some View { VStack (spacing: 40) { Text(title) .font(.title2) .bold() .foregroundColor(.black) HStack { Image(systemName: "arrow.left.square.fill") .resizable() .scaledToFit() .frame(width: 20) .foregroundColor(.gray) ScrollView (.horizontal) { HStack (spacing: 20) { ForEach(goods.indices) { item in NavigationLink { goodContent(good: goods[item]) } label: { VStack (spacing: 10) { Image("\(goods[item].imageCode)-1") .resizable() .scaledToFill() .frame(width: 120, height: 120) .clipShape(RoundedRectangle(cornerRadius: 15)) Text(goods[item].title) .foregroundColor(Color(.black)) Text("$\(goods[item].oldPrice)") .strikethrough(true, color: .gray) .foregroundColor(.gray) Text("$\(goods[item].newPrice)") .foregroundColor(.red) } } } } .listStyle(PlainListStyle()) .background(Color(.white)) } .listRowBackground(Color(.white)) Image(systemName: "arrow.right.square.fill") .resizable() .scaledToFit() .frame(width: 20) .foregroundColor(.gray) } .background(Color(.white)) } } } ``` ::: ### 登入系統的帳號密碼檢查機制 - 檢查使用者輸入之帳號或密碼是否為空 - 使用 `guard` 語法 - 檢查該帳號是否存在 - 註冊後會將該帳號 `append` 至資料庫中 - 若帳號存在,檢查密碼是否正確 :::spoiler 程式碼 ```swift= // 檢查帳號密碼是否合法以及登入是否成功 private func checkCredentials() { // 檢查帳號或密碼欄位是否為空白 guard !user.isEmpty && !pwd.isEmpty else { alertMsg = "帳號或密碼不能為空白" return } if status == "登入" { for item in MyView.accounts.indices { if MyView.accounts[item].username == user { if MyView.accounts[item].password == pwd { alertMsg = "登入成功" MyView.username = user login = true } else { alertMsg = "密碼不正確" } break } else { alertMsg = "帳號不存在" } } } else { alertMsg = "註冊成功" MyView.accounts.append(account(username: user, password: pwd, accumulation: 0)) MyView.username = user login = true } } ``` ::: ### 關於單個商品的顯示介面 - 點擊圖片瀏覽可跳轉至下一張或上一張圖片 - 購物車系統 - 使用 Picker 選擇商品尺寸 - 設立防呆機制要求必須選擇商品尺寸 :::spoiler 程式碼 ```swift= // 顯示特定單一商品頁面 struct goodContent: View { @State private var page: Int = 1 @State private var showAlert: Bool = false @State private var alertMsg: String = "Sucessful!" @State private var selectedSize: Int = 0 @State var good: Good let selections = ["選擇尺寸", "M", "L"] var body: some View { ZStack { Color(.white) ScrollView (showsIndicators: false) { ZStack { Color(.white) VStack { // 商品預覽圖可透過點擊左右箭頭按鈕切換該商品預覽圖 HStack (spacing: 3) { // 左箭頭按鈕 Button { page -= 1 if page == 0 { page = good.imageLength } } label: { Image(systemName: "arrow.left.square.fill") .resizable() .scaledToFit() .frame(width: 25, height: 25) .foregroundColor(.gray) } // 右箭頭按鈕 Button { page += 1 if page > good.imageLength { page = 1 } } label: { Image("\(good.imageCode)-\(page)") .resizable() .scaledToFill() .frame(width: 400, height: 400) .clipShape(RoundedRectangle(cornerRadius: 10)) Image(systemName: "arrow.right.square.fill") .resizable() .scaledToFit() .frame(width: 25, height: 25) .foregroundColor(.gray) } } Text(good.title) .font(.title) .bold() .foregroundColor(.black) .padding(EdgeInsets(top: 40, leading: 0, bottom: 0, trailing: 0)) HStack (spacing: 25) { Text("$\(good.oldPrice)") .font(.title3) .strikethrough() .foregroundColor(.black) Text("$\(good.newPrice)") .font(.title2) .bold() .foregroundColor(.red) } Picker(selection: $selectedSize) { Text("選擇尺寸").tag(0) Text("M").tag(1) Text("L").tag(2) } label: { Text("選擇尺寸") } // 若有尚未選擇商品尺寸則不能加入購物車 Button { good.size = selections[selectedSize] if good.size == "選擇尺寸" { alertMsg = "請選擇尺寸" } else { alertMsg = "成功加入購物車" cartView.cart.append(good) cartView.total += good.newPrice } showAlert = true } label: { ZStack { RoundedRectangle(cornerRadius: 8) .frame(width: 125, height: 50) Text("加入購物車") .foregroundColor(.white) } } .alert(alertMsg, isPresented: $showAlert) { Button ("OK") { showAlert = false } } } } .background(Color(.white)) } } .background(Color(.white)) } } ``` ::: ### 登入系統 - 配合登入帳密檢查機制要求使用者登入 - 登入後顯示使用者資訊 - 配有登出功能 :::spoiler 程式碼 ```swift= // 顯示登入系統畫面及使用者帳戶畫面 struct MyView: View { @State private var login = false @State private var showLoginView = false @State private var status = "登入" @State private var showAlert = false public static var username = "unknown" public static var accounts = [account.demoAccount] var body: some View { NavigationStack { ZStack { Color(.white) if !login { // 使用者登入前的介面 VStack (spacing: 40) { Text("請先登入") .font(.title) .bold() .foregroundColor(.black) HStack (spacing: 20) { ButtonView(login: $login, status: "註冊") ButtonView(login: $login, status: "登入") } } .background(Color(.white)) } else { // 使用者登入後的介面 VStack (alignment: .leading, spacing: 40) { Text("我的帳號") .foregroundColor(Color(.black)) .font(.title) .bold() .padding(EdgeInsets(top: 25, leading: 25, bottom: 0, trailing: 200)) List { HStack (spacing: 0) { Group { Text("我的帳戶:") Text(MyView.username) } .foregroundColor(.black) .listRowBackground(Color(.white)) } .listRowBackground(Color(.white)) // 導向使用者購物車介面 NavigationLink { cartView() } label: { Text("我的購物車") .foregroundColor(.black) } .listRowBackground(Color(.white)) // 登出當前帳戶按鈕 Button { showAlert = true login = false } label: { Text("登出") .foregroundColor(.black) } .listRowBackground(Color.white) } .listStyle(PlainListStyle()) .listRowBackground(Color(.white)) } .background(Color(.white)) } } .background(Color(.white)) .alert("成功登出", isPresented: $showAlert) { Button ("OK") { } } } } } ``` ::: ### 購物車系統 - 顯示使用者購物車清單 - 下單功能 - 設立防呆機制防止無效下單 :::spoiler 程式碼 ```swift= // 顯示使用者購物車介面 struct cartView: View { @State private var showAlert: Bool = false @State private var AlertMsg: String = "購物車中沒有商品" public static var total = 0 public static var cart = [Good(title: "", imageCode: "", oldPrice: 0, newPrice: 0, imageLength: 0)] var body: some View { ZStack { Color(.white) VStack (alignment: .trailing) { display() // 按下下單按鈕後檢查購物車是否為空 Button { if cartView.total == 0 { AlertMsg = "購物車中沒有商品" } else { AlertMsg = "成功下單!" } showAlert = true } label: { ZStack { RoundedRectangle(cornerRadius: 10) .frame(width: 70, height: 40) Text("結帳") .foregroundColor(.white) } .padding(EdgeInsets(top: 0, leading: 0, bottom: 20, trailing: 20)) } .alert(AlertMsg, isPresented: $showAlert) { Button ("OK") { showAlert = false } } } } .background(Color(.white)) } // 顯示購物車清單 private func display() -> some View { ZStack { Color(.white) List { ForEach(cartView.cart.indices) { item in if item != 0 { HStack { Image("\(cartView.cart[item].imageCode)-1") .resizable() .scaledToFill() .frame(width: 100, height: 100) .clipShape(RoundedRectangle(cornerRadius: 10)) VStack (alignment: .leading, spacing: 15) { Text("\(cartView.cart[item].title)") .font(.title2) .foregroundColor(.black) HStack (spacing: 15) { Group { Text("Size: \(cartView.cart[item].size)") Text("$\(cartView.cart[item].newPrice)") } .foregroundColor(.black) } } } } } .background(Color(.white)) .listRowBackground(Color(.white)) Text("total: $\(cartView.total)") .foregroundColor(.black) .background(Color(.white)) .listRowBackground(Color(.white)) } .background(Color(.white)) .listStyle(PlainListStyle()) .listRowBackground(Color(.white)) } } } ``` ::: <!-- {%hackmd /@Ateto/Style %} -->