SwiftUI 原则

四大原则

  • 声明式( declarative )
  • 自动化 ( automatic )
  • 组合 ( composition )
  • 一致 ( consistent )

#SwiftUI 背后的 Swift 5.1

不透明的返回类型 (opaque return type)

import Cocoa

protocol P {}

extension Int: P {}

extension String: P {}

func add1() -> some P {
    return "some value"
}

func add2() -> some P {
    return 110
}


let res: P = add1()
let res2: P = add2()

print(res)
print(res2)

函数所有返回路径上返回的值应该是相同的具体类型, 而不能是不同的类型

这不同于返回 协议(protocol) . 只返回协议类型的话, 可以是协议的任一类型, 而 opaque 则返回的是同一类型, 虽然也不确定是哪一个类型.

##从单一表达式函数隐式返回

简单地说, 就是从一个只有一个表达式的函数中不需要添加 return 关键字来返回. 例如

func add(num: Int, num2: Int) -> Int {
  num + num2
}

函数构建器

简单说它是特殊的带注解的函数, 用于组件序列隐式地构建值. 这些组件通常采用语句和表达式的形式, 它们一起生成单一值. 这是基于构建器 设计模式.

let cell = StandardCell() 
.useTitle(cellData.title) 
.useSubtitle(cellData.subtitle) 
.useImage(cellData.image) 
.build()

特定领域语言 DSL

构建器也特别适合用于 DSL

属性包装器 property Wrappers

import Cocoa

@propertyWrapper
struct Capitalised {
    private var value: String
    init(wrappedValue value: String) {
        self.value = value.capitalized
    }
    var wrappedValue: String {
        get { value }
        set { value = newValue.capitalized }
    }
}


struct User {
    @Capitalised
    var firstName:String
    @Capitalised
    var lastName:String
}

View 和 Controls

在 SwiftUI 中, 几乎所有东西都是一个 view , 这不同于传统的 Objective-C 和 UIKit 的 view. 称为 view , 不同于 iOS 中的 UIView 或 MacOS 中的 NSView , 它不同一个 struct 或 class , 实际上, 它是 protocol

几乎所有符合 View 协议的对象, 都有一个关联类型变量称为 body , body 它再次返回一个 View.

修改器 modifiers

Text

struct ContentView: View {
    var body: some View {
        Text("Hello, World! 你好,  世界! 修改一下咯.")
            .background(Color.red)
            .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

Images

import SwiftUI

struct ContentView: View {
    var body: some View {
        Image("fuck")
            .frame(width: 800,
        height: 800,
        alignment: .center)
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Image 名字 fuck , 是由在 Assets.xcassets 里的资源名来决定的.

image-20200107153706763

image-20200107153545038

Buttons

struct ContentView: View {
    var body: some View {
        Button("点我") {
            // handle
            print("点击事件...")
        }
    }
}

Toggle

struct ContentView: View {
    @State var isOn = false
    
    var body: some View {
        Toggle(isOn: $isOn) {
            if isOn {
                Text("开启中....")
            } else {
                Text("关闭中....")
            }
        }
    }
}

TextField / SecureField

struct ContentView: View {
    @State var name = "Unknown"
    
    var body: some View {
        TextField("input your name", text: $name){
            print("你输入的是 \(self.name)")
        }
    }
}

Slider

struct ContentView: View {
    @State var progress = 0.0
    
    var body: some View {
        VStack {
            Slider(value: $progress, in: -100...100, step: 10)
            Text("progress is \(progress)")
        }
    }
}

Stepper

struct ContentView: View {
    @State var steps = 0
    
    var body: some View {
        Stepper("Steps \(steps)", value: $steps, in: 0...100)
    }
}

Picker

struct ContentView: View {
    @State var college = 0
    var unis = ["JCU", "CQU"]
    
    var body: some View {
        Picker(selection: $college, label: Text("Hello label"), content: {
            ForEach(0..<unis.count) { uni in
                Text(self.unis[uni]).tag(uni)
            }
        })
    }
}

DatePicker

struct ContentView: View {
    @State var whensTheDate = Date()
    var unis = ["JCU", "CQU"]
    
    var body: some View {
        DatePicker("When is the big day?",
        selection: $whensTheDate,
        in: Date()...,
        displayedComponents: [.date, .hourAndMinute])
    }
}
struct ContentView: View {
    
    var body: some View {
        
    }

}

TabView

struct ContentView: View {
    
    var body: some View {
        TabView{
            Text("First Controller View")
                .tabItem({Text("first")})
            Text("Second Controller View")
                .tabItem({Text("second")})
        }
    }
}

Stacks

HStack

struct ContentView: View {
    
    var body: some View {
        HStack { 
          Text("Hello")
        	Text("World") 
        }
    }

}

VStack

struct ContentView: View {
    
    var body: some View {
        VStack {
            Text("Hello")
            Text("World")
        }
    }

}

ZStac

struct ContentView: View {
    
    var body: some View {
        ZStack {
            Text("Hello")
            Text("World")
                .offset(x: 0, y: 20)
        }
    }

}

Decorators

Spacer

struct ContentView: View {
    
    var body: some View {
        VStack { Text("Hello")
        Spacer()
        Text("World") }

    }

}

Divider

struct ContentView: View {
    
    var body: some View {
        HStack { Text("Hello")
        Divider()
        Text("World") }

    }

}

Alert

struct ContentView: View {
    
    @State var showAlert = false
    
    var body: some View {
        Button("Show Alert") {
            self.showAlert = true
        }.alert(isPresented: $showAlert) {
                Alert(title: Text("Alert Box"),
                      message: Text("This is our first Alert Message"),
                      dismissButton: .default(Text("OK")) )
        }
    }
    
}

List

struct ContentView: View {
    
    var body: some View {
        List(0..<50) { item in
            Text("Item # \(item)")
        }
    }
    
}

Section

struct ContentView: View {
    
    var body: some View {
        Section(header: Text("Header"),
        footer: Text("this is a footer")) {
        Text("This is the sections’ contents") }
    }
    
}

Data 和 Combine

@State

它本质上是一个 Property Wrapper.

表示变量可读写.

更新 View

image-20200107173907631

@Binding

struct ExtractedView: View {
    @Binding var booked: Bool
		var body: some View {
		Button(booked ? "Release" : "Book") {
			self.booked.toggle() }
		} 
}

ObservableObject 协议

class BookingStore: ObservableObject {
    var objectWillChange = PassthroughSubject<Void, Never>()
    var bookingName: String = "" {
        didSet { updateUI() }
    }
    var seats: Int = 1 {
        didSet { updateUI() }
    }
    func updateUI() {
        objectWillChange.send()
   	}
    
}


struct AnotherView: View {
    @ObservedObject var model = BookingStore()
    var body: some View {
            VStack {
                TextField("Your Name", $model.bookingName)
                Stepper("Seats : \(model.seats)",
                    value: $model.seats, in: 1...5)
            }
    }
}

State Vs Bindable

@State 是 view 本地的 (local). 即数据是被 View 本地持有的. 由于它是本地存储的, 所以它是一个`值类型(value type) 由框架 Framework 自动管理.

@ObservableObject 是在其他外部 View 的, 并没有保存在本地 View. 它是一个引用类型(reference type) . 它并不保存在本地, 只是简单的一个引用. 由开发者管理. 最好只用于外部的数据, 比如数据库

EnvironmentObject

image-20200107180031516

Combine

它的核心概念是 PublishersSubscribers

通过简单地在任意属性里添加注解 @Published 即可成为 Publisher . 它会自动同步 objectWillChange publisher

布局和展示

SwiftUI 使用 Flexible Box 布局系统(像 Web )

元素和修改器

Text("Hello World")

图片剪裁形状

Image("user")
.resizable()
.frame(width: 250, height: 250) .clipShape(Circle())

其他的看 API 文档

GeometryReader

struct ContentView: View {
    
    var body: some View {
        ScrollView {
                HStack {
        ForEach(0..<15){ index in GeometryReader { g in
        Text("This is item: \(index)") .rotationEffect(.degrees(
        Double(g.frame(in: .global).minX) ))
            }
            }.frame(width: 300, height: 300)
            } }.background(Color.orange)
        
//            .frame(width: 800, height: 800, alignment: .center)
    }
    
}

画图和动画

Timer

传统上, 通过定时器来刷新画面

struct ContentView: View {
    @State private var counter = 0
    
    let timer = Timer.publish(every: 1, on: .current, in: .common).autoconnect()
    var body: some View {
        Text("Counter Ticks : \(counter)")
        .onReceive(timer) { _ in
            self.counter += 1
        }
    }
    
}

形状

Rectangle

struct ContentView: View {
    var body: some View {
        Rectangle()
        .fill(Color.black)
        .frame(width: 200, height: 200)
    }
    
}

RoundedRectangle

struct ContentView: View {
    var body: some View {
        RoundedRectangle(cornerSize: CGSize.init(width: 35, height: 35))
        .frame(width: 200, height: 200)
    }
    
}

Circle

struct ContentView: View {
    var body: some View {
        Circle()
        .frame(width: 200, height: 200)
    }   
}

Ellipse

struct ContentView: View {
    var body: some View {
        Ellipse()
        .fill()
        .frame(width: 200, height: 200)
    }
    
}

Capsule

struct ContentView: View {
    var body: some View {
        Capsule()
        .fill()
        .frame(width: 200, height: 200)
    }
    
}

形状修改器

Frame

修改形状的大小. 它会创建固定大小的形状

Clipped

限制内容并剪辑它. 可确保内容是容器的大小, 不会溢出

Trim

根据点参数 from, to 剪掉指定形状. 比如对于圆形来说

image-20200108104015396

Stroke

struct ContentView: View {
    var body: some View {
        Rectangle()
            .stroke(lineWidth: CGFloat.init(10.0))
            .fill(Color.red)
    }
    
}

image-20200108104642424

LineJoin / LineCap / DashPhase

struct ContentView: View {
    var body: some View {
        Rectangle()
        .stroke(style: StrokeStyle(
        lineWidth: 2, lineCap: .round, lineJoin: .round, dash: [0, 5], dashPhase: 0
        ) )
        .frame(width: 200, height: 200)

    }
}

动画

struct ContentView: View {
    @State private var jiggle = false
    var body: some View {
        Text("Hello World!")
            .scaleEffect(jiggle ? 1.0 : 0.3)
            .animation(Animation.spring().repeatForever())
            .onAppear() {
            self.jiggle.toggle()
        }
        .frame(width: 200, height: 200, alignment: .center)
    }
    
}

Path

struct ContentView: View {
    var body: some View {
        Path { path in
            path.move(to: CGPoint(x: 10, y: 10))
            path.addLine(to: CGPoint(x: 10, y: 210))
            path.addLine(to: CGPoint(x: 210, y: 210))
            path.addLine(to: CGPoint(x: 210, y: 10))
        }
        .frame(width: 400, height: 400, alignment: .center)
    }
    
}

交互

Tap

点击

struct ContentView: View {
    var body: some View {
        Text("Hello World!")
            .onTapGesture {
                print("Text was tapped")
            }
        .frame(width: 400, height: 400, alignment: .center)
    }
    
}

LongPress

长按

struct ContentView: View {
    var body: some View {
        Text("Hello World!")
            .onLongPressGesture() {
                print("Text was long press")
            }
        .frame(width: 400, height: 400, alignment: .center)
    }
    
}

Drag

拖动

struct ContentView: View {
    var body: some View {
        Text("Hello World!")
            .gesture(
                DragGesture(minimumDistance: 60)
                    .onEnded { drag in
                        print(drag)
                    }
            )
        .frame(width: 400, height: 400, alignment: .center)
    }
    
}

Rotation

.gesture( RotationGesture( minimumAngleDelta: Angle. degrees(1))

Hover

struct ContentView: View {
    var body: some View {
       Text("Hello World")
        .onHover{ _ in print("Hovering over the element")}
       .frame(width: 400, height: 400, alignment: .center)
    }
    
}

Appearing / Disappearing

.onAppear {
print("The view has appeared on screen")
}
.onDisappear {
print("The view has appeared on screen")
}

当 view 做准备显示到屏幕/关闭显示时触发

访问 API 数据

访问 URL

import Cocoa

let url = "https://icanhazdadjoke.com/"
var urlRequest = URLRequest(url: URL.init(string: url)!)
urlRequest.addValue("text/plain",
                    forHTTPHeaderField: "Accept")
URLSession.shared.dataTask(with: urlRequest) { data, response, error in
if let data = data,
let httpResponse = response as? HTTPURLResponse, (200..<300) ~= httpResponse.statusCode,
let strData = String(bytes: data, encoding: .utf8)
        {
            print(strData)
} }.resume()

处理 JSON

struct Joke: Codable {
        var id: String
        var joke: String
        var status: Int
    }

let json = try? JSONDecoder().decode(Joke.self, from: data) { 
  print(json)
}

参考资料