# #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 %} -->