SwiftUI教程一:搭建一个简单的应用
1 目的
使用SwiftUI搭建一个应用,从这个应用中,先熟悉SwiftUI如何使用,以及部分与UIKit的区别。
2 搭建
2.1 新建一个SwiftUI的项目
打开Xcode -> Create a new Xcode project -> iOS -> Single View App
![](https://img.haomeiwen.com/i3687616/dd59ca6f1ebab478.png)
Language选择swift,User Interface选择SwiftUI,输入相关信息,选择Next创建新项目。
点击预览界面Resume,即可查看预览。
![](https://img.haomeiwen.com/i3687616/28d5394c1b0c0aa3.png)
初始项目预览如下:
![](https://img.haomeiwen.com/i3687616/b9a5ca8f5bc87523.png)
2.2 创建出一个列表
先看一下左侧的导航栏
![](https://img.haomeiwen.com/i3687616/c2cc8c011d809f65.png)
和UIKit项目类似,但是没有了ViewController,也没有storyboard,点开AppDelegate和SceneDelegate,查看,发现在SceneDelegate里有如下代码:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
-
什么是SceneDelegate?
我的理解是,以前一个App就占用了一个Window,由于现在有了分屏功能,一个Window上可以多个App,一个App所占用的内容,就是一个Scene。SceneDelegate作用和以前的AppDelegate类似。 -
界面是如何呈现的?
可以看到上方代码,首先创建出内容let contentView = ContentView()
,然后转换scene为UIWindowScene,创建出UIWindow,设置window的rootViewConroller,最后呈现。 -
UIHostingController是做什么的?
点开UIHostingController类,可以看到如下内容:
@available(iOS 13.0, tvOS 13.0, *)
@available(OSX, unavailable)
@available(watchOS, unavailable)
open class UIHostingController<Content> : UIViewController where Content : View {
public init(rootView: Content)
public init?(coder aDecoder: NSCoder, rootView: Content)
@objc required dynamic public init?(coder aDecoder: NSCoder)
@objc override dynamic open func viewWillAppear(_ animated: Bool)
public var rootView: Content
public func sizeThatFits(in size: CGSize) -> CGSize
@objc override dynamic open var preferredStatusBarStyle: UIStatusBarStyle { get }
@objc override dynamic open var prefersStatusBarHidden: Bool { get }
@objc override dynamic open var preferredStatusBarUpdateAnimation: UIStatusBarAnimation { get }
@objc override dynamic public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)
}
可以得出很多信息:
1.该类适用iOS 13+,tvOS 13+,而OSX、watchOS不能使用。
2.该类继承自UIViewController。
3.rootView是View。
4.该类的方法能看出,该类可以控制顶部状态栏状态、显隐等。
可以得出结论,该类是控制SwiftUI显示的VC,SwiftUI的内容需要通过该类来显示,作用同UIKite的VC。
接下来看看ContentView的内容:
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello, World!")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
- 1.什么是View?
点开View查看内容:
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol View {
associatedtype Body : View
var body: Self.Body { get }
}
可以看到,View是一个协议,其中有一个泛型属性Body,在重写body属性时,SwiftUI会自己判断出Body的类型,还有一个属性body,重写该属性get方法,在get方法内,控制界面的显示与动作等。
- ContentView_Previews干嘛的?
我的理解是,该结构体是用于SwiftUI预览时,传递一个些必要属性使用的。该类可以删除,删除后也就无法使用预览功能了。
- ContentView_Previews干嘛的?
接下来,创建出一个列表,内容包含5条Hello World!。代码如下:
struct ContentView: View {
var body: some View {
List {
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
}
}
}
预览如下:
![](https://img.haomeiwen.com/i3687616/8cbd91d22f94b130.png)
- 什么是List?
打开List定义:
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct List<SelectionValue, Content> : View where SelectionValue : Hashable, Content : View {
@available(watchOS, unavailable)
public init(selection: Binding<Set<SelectionValue>>?, @ViewBuilder content: () -> Content)
@available(watchOS, unavailable)
public init(selection: Binding<SelectionValue?>?, @ViewBuilder content: () -> Content)
public var body: some View { get }
public typealias Body = some View
}
1.List继承自View,说明是SwiftUI的内容。
2.List是一个容器,能够排列显示单行内容,效果类似于UIKit的UITableView。
- 什么是Text?
先看定义:
extension Text {
//设置文字颜色
public func foregroundColor(_ color: Color?) -> Text
//设置文字字体
public func font(_ font: Font?) -> Text
//设置文字宽度
public func fontWeight(_ weight: Font.Weight?) -> Text
//设置文字粗细
public func bold() -> Text
//设置文字斜体
public func italic() -> Text
//设置文字删除线
public func strikethrough(_ active: Bool = true, color: Color? = nil) -> Text
//设置文字下划线
public func underline(_ active: Bool = true, color: Color? = nil) -> Text
//设置文字字距
public func kerning(_ kerning: CGFloat) -> Text
//设置单词间隔
public func tracking(_ tracking: CGFloat) -> Text
//设置基准线偏移
public func baselineOffset(_ baselineOffset: CGFloat) -> Text
}
Text继承自View,说明是SwiftUI的内容。作用类似于UIKit的UILabel。
2.3 创建导航栏
如下代码即可创建出导航栏
struct ContentView: View {
var body: some View {
NavigationView {
List {
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
}
}
}
}
预览图:
![](https://img.haomeiwen.com/i3687616/0eccc89db56f0e8d.png)
接下来设置标题,由于是给NavigationView包含着List,所以标题应该是由List来设置的。
struct ContentView: View {
var body: some View {
NavigationView {
List {
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
}.navigationBarTitle(Text("Hello World!"))
}
}
}
预览图:
![](https://img.haomeiwen.com/i3687616/a4bcbe273bc4022b.png)
- 什么是NavigationView?
打开定义:
public struct NavigationView<Content> : View where Content : View {
public init(@ViewBuilder content: () -> Content)
public typealias Body = Never
}
可以看出是SwiftUI特有的,作用类似于UIKit的UINavigationVontroller。管理界面跳转等做用。
- navigationBarTitle方法做什么的?
打开定义:
public func navigationBarTitle(_ title: Text) -> some View
可以看到,该方法需要传入一个Text,然后将它在顶部导航栏上显示出来。
2.4 显示图片名称列表
我们拖4张图片到项目中,然后将图片名字在列表中显示出来。
![](https://img.haomeiwen.com/i3687616/313a06549f67d060.png)
创建出数据对象
import Combine
class DataSource: ObservableObject {
let didChange = PassthroughSubject<Void, Never>()
var picture = [String]()
init() {
let fm = FileManager.default
if let path = Bundle.main.resourcePath, let items = try? fm.contentsOfDirectory(atPath: path) {
items.forEach {
if $0.hasSuffix("jpg") {
picture.append($0)
}
}
}
didChange.send()
}
}
- 什么是ObservableObject?
ObservableObject是一个协议,表示该类是可被订阅的。 - 什么是PassthroughSubject?
作用是向订阅者发出广播值。 - Combine模块的作用?
Combine模块作用类似于RxSwift,是一个函数式反应式编程库。 - didChange.send()做什么的?
向订阅者发出广播值。
将数据与界面进行绑定
struct ContentView: View {
@ObservedObject var dataSource = DataSource()
var body: some View {
NavigationView {
List(dataSource.picture, id: \.self) {
Text($0)
}.navigationBarTitle(Text("Hello World!"))
}
}
}
- 什么是ObservedObject?
ObservedObject表示该属性是可以被View绑定。 - 如何给List指定数据源?
创建List的时候,指定数据源。 - 如何重用的?
创建List的时候,id传入唯一标识的KeyPath。
预览:
![](https://img.haomeiwen.com/i3687616/b8ec0c34bb439ef7.png)
2.5 跳转图片查看页面
新建一个查看页View
struct DetailView: View {
var iamgeName: String
var body: some View {
let image = UIImage(named: iamgeName)
return Image(uiImage: image!)
.resizable()
.aspectRatio(1024/768,contentMode: .fit)
.navigationBarTitle(Text(iamgeName), displayMode: .inline)
}
}
Image在SwiftUI中是图片控件,等同于UIKit的UIImageView。
resizable方法,使Image能够进行改变,不调用该方法,则Image大小固定。
aspectRatio方法,设置Image内图片的缩放方法。
增加跳转代码
struct ContentView: View {
@ObservedObject var dataSource = DataSource()
var body: some View {
NavigationView {
List(dataSource.picture, id: \.self) {
NavigationLink($0, destination: DetailView(iamgeName: $0))
}.navigationBarTitle(Text("Hello World!"))
}
}
}
使用NavigationLink即可,初始化的时候指定跳转到DetailView。
![](https://img.haomeiwen.com/i3687616/e5eb410f7845f598.png)
2.6 给图片增加点击事件
struct DetailView: View {
var iamgeName: String
@State var hideTitle = false
var body: some View {
let image = UIImage(named: iamgeName)
return Image(uiImage: image!)
.resizable()
.aspectRatio(1024/768,contentMode: .fit)
.navigationBarTitle(Text(iamgeName), displayMode: .inline)
.navigationBarHidden(hideTitle)
.onTapGesture {
self.hideTitle.toggle()
}
}
}
添加一个hideTitle属性,使用@State属性装饰器修饰,表示该值能够与View进行绑定。
hideTitle.toggle()对hideTitle进行修改。
![](https://img.haomeiwen.com/i3687616/5d08413ae65f1027.png)
点击图片,则上方title消失。
3 全部代码
import SwiftUI
import Combine
class DataSource: ObservableObject {
let willChange = PassthroughSubject<Void, Never>()
var picture = [String]()
init() {
let fm = FileManager.default
if let path = Bundle.main.resourcePath, let items = try? fm.contentsOfDirectory(atPath: path) {
items.forEach {
if $0.hasSuffix("jpg") {
picture.append($0)
}
}
}
willChange.send()
}
}
struct ContentView: View {
@ObservedObject var dataSource = DataSource()
var body: some View {
NavigationView {
List(dataSource.picture, id: \.self) {
NavigationLink($0, destination: DetailView(iamgeName: $0))
}.navigationBarTitle(Text("Hello World!"))
}
}
}
struct DetailView: View {
var iamgeName: String
@State var hideTitle = false
var body: some View {
let image = UIImage(named: iamgeName)
return Image(uiImage: image!)
.resizable()
.aspectRatio(1024/768,contentMode: .fit)
.navigationBarTitle(Text(iamgeName), displayMode: .inline)
.navigationBarHidden(hideTitle)
.onTapGesture {
self.hideTitle.toggle()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}