天天看点

SwiftUI 初体验 2 列表使用 / 设备预览设置

首先要创建List里面嵌套的RowView,创建SwiftUI类命名为RowView。

struct RowView: View {
    
    var species: Species
    
    var body: some View {
        HStack {
            species.image()
                .resizable()
                .aspectRatio(1, contentMode: .fit)
                .frame(width: 50, height: 50, alignment: .center)
                .clipShape(Circle())
                .padding(EdgeInsets(top: 8, leading: 15, bottom: 8, trailing: 8))
            Text(species.description)
            Spacer()
        }
    }
    
}
           

结构体中添加属性species物种,description描述。在body中添加Image和Text组合的Stack。为了预览效果我们可以在PreviewProvider中修改预览样式,其中previewLayout只是在预览时设置相关属性。

struct RowView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            RowView(species: "lion", description: "The lion is a ferocious beast")
            RowView(species: "horse", description: "The horse run fast")
        }
        .previewLayout(.fixed(width: 400, height: 80))
    }
}
           

效果如下:

SwiftUI 初体验 2 列表使用 / 设备预览设置

其中使用Group可以展示多个预览,previewLayout可以跟在单个项目中,也可以跟在Group后面。

为了方便先在Xcode中新建一个文件,重命名为AnimalsData.json,里面模拟一份json数据。为了在Bundle.main中加载这个文件,必须在target的BuildPhases的CopyBundleResources中添加引用。

[
    {
        "id": 1001,
        "name": "cat",
        "imageName": "cat",
        "description": "Cats can rule humans"
    },
    {
        "id": 1002,
        "name": "cat",
        "imageName": "dog",
        "description": "Dog is a friend of mankind"
    },
    {
        "id": 1003,
        "name": "cat",
        "imageName": "elephant",
        "description": "The size of elephant is very huge"
    },
    {
        "id": 1004,
        "name": "cat",
        "imageName": "horse",
        "description": "Horses run fast"
    },
    {
        "id": 1005,
        "name": "cat",
        "imageName": "lerpard",
        "description": "Lerpard is a beast that eats meat"
    },
    {
        "id": 1006,
        "name": "cat",
        "imageName": "lion",
        "description": "The lion is a ferocious beast"
    },
    {
        "id": 1007,
        "name": "cat",
        "imageName": "mouse",
        "description": "The mouse is very small"
    },
    {
        "id": 1008,
        "name": "cat",
        "imageName": "rabbit",
        "description": "Rabbits is the food that beast like"
    },
    {
        "id": 1009,
        "name": "cat",
        "imageName": "tiger",
        "description": "Tiger is a carnivorous animal"
    },
    {
        "id": 1010,
        "name": "cat",
        "imageName": "wolf",
        "description": "wolves live together"
    }
]

           

新建一个Data.swift文件,作为数据源读取的工具类。

//
//  Data.swift
//  SwiftUIDemo
//
//  Created by Eugene on 2019/9/28.
//  Copyright © 2019 Eugene. All rights reserved.
//

import SwiftUI
import UIKit

let speciesData: [Species] = load("AnimalsData.json")

func load<T: Decodable>(_ filename: String, as type: T.Type = T.self) -> T {
    
    let data: Data
    
    guard let file = Bundle.main.url(forResource: filename, withExtension: nil) else {
        fatalError("Couldn't find \(filename) in main bundle.")
    }
     
    do {
        data = try Data(contentsOf: file)
    } catch {
        fatalError("Could't load \(filename) from main bundle: \n \(error)")
    }
    
    do {
        let decoder = JSONDecoder()
        return try decoder.decode(T.self, from: data)
    } catch {
        fatalError("Coludn't parse \(filename) as \(T.self):\n\(error)")
    }
    
}

final class ImageStore {
    
    typealias _ImageDictionary = [String: CGImage]
    fileprivate var images: _ImageDictionary = [:]
    fileprivate static var scale = 2
    static var shared = ImageStore()
    
    static func loadImage(name: String) -> CGImage {
        
        guard
            let url = Bundle.main.url(forResource: name, withExtension: nil),
            let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
            let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil)
        else {
            fatalError("Couldn't load iamge \(name) from main bundle")
        }
        
        return image
        
    }
    
    fileprivate func _guaranteeImage(name: String) -> _ImageDictionary.Index {
        
        if let index = images.index(forKey: name) {
            return index
        }
        
        images[name] = ImageStore.loadImage(name: name)
        
        return images.index(forKey: name)!
        
    }
    
    func image(name: String) -> Image {
        
        let index = _guaranteeImage(name: name)
        
        return Image(images.values[index], scale: CGFloat(ImageStore.scale), label: Text(verbatim: name))
        
    }
    
}

           

这个工具类可以提供我们需要的数据源,在demo里当是模拟后端返回的数据了。当然实体类的decode和encode有它的规则,需要遵循协议。下面把实体类也贴上来。

import SwiftUI

struct Species: Hashable, Codable, Identifiable {
    
    var id: Int
    var name: String
    var description: String
    var imageName: String
    
}

extension Species {
    
    func image() -> Image {
        let picture = Image.init(imageName)
        return picture
    }
    
}

           

到这里准备工作就差不多了。回到AnimalList页面,编辑代码:

import SwiftUI

struct AnimalList: View {
    var body: some View {
        NavigationView {
            List(speciesData) { species in
                NavigationLink(destination: AnimalDetails()) {
                    RowView(species: species)
                }
            }
            .navigationBarTitle(Text("Animals"))
        }
    }
}

struct AnimalList_Previews: PreviewProvider {
    static var previews: some View {
        AnimalList()
    }
}

           

首先List(speciesData) { species in …} 是遍历数据源speciesData以展示列表,(speciesData的读取测试可以新建一个TestCase尝试把它print出来,里面的fatalError可以帮助我们准确跟踪到哪一步出了问题。在我编辑上面的json文件时就多次出现格式错误,通过fatalError可以快速定位。建议大家养成良好的使用测试用例的习惯,有助于加强我们的代码健壮性。)通过NavigationLink(destination: …) { cell的view} 我们可以绑定click事件和响应。这个Link包裹住的cell有自动生成的实例方法,(传进去的参数就是我们上面的实体类Species,所以Cell的编写最好有良好的格式习惯)我们把遍历的项目传递进去每一个cell,就可以准确的展示json的内容。

其中配合NavigationLink使用之前必须用NavigationView嵌套页面,表示当前控制器以NavigationController调配控制。而NavigationView里面的项目,可以通过navigationBarTitle来标注其标题。

预览效果如下:

SwiftUI 初体验 2 列表使用 / 设备预览设置

在navigation进行push的时候,数据的传递跟上面也是类似的,先写一个简单的details页面

import SwiftUI

struct AnimalDetails: View {
    
    var species: Species
    
    var body: some View {
        VStack {
            HStack {
                species.image()
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .clipShape(Rectangle())
                    .clipped()
            }
            VStack {
                Text(species.description)
                    .fontWeight(.medium)
                    .font(.system(size: 24))
            }
            Spacer()
        }
    }
    
}

struct AnimalDetails_Previews: PreviewProvider {
    static var previews: some View {
        AnimalDetails(species: speciesData[0])
    }
}

           

可以看到preview provider中的初始化方法也跟着改变了,那么list页面的代码稍作修改:

struct AnimalList: View {
    var body: some View {
        NavigationView {
            List(speciesData) { species in
                NavigationLink(destination: AnimalDetails(species: species)) {
                    RowView(species: species)
                }
            }
            .navigationBarTitle(Text("Animals"))
        }
    }
}
           

在创建AnimalDetails实例的时候就会把遍历的entity传递过去。live preview就可以进行交互看到效果:

SwiftUI 初体验 2 列表使用 / 设备预览设置

顺带一提的是,preview中可以同时对使用的device和名称进行指定:

struct AnimalList_Previews: PreviewProvider {
    static var previews: some View {
        ForEach(["iPhone 8", "iPhone 11 Max"], id: \.self) { deviceName in
            AnimalList()
                .previewDevice(PreviewDevice(rawValue: deviceName))
                .previewDisplayName(deviceName)
        }
    }
}

           
SwiftUI 初体验 2 列表使用 / 设备预览设置

这似乎就是Apple这个大版本的其中一个很不错的卖点,为开发者跨平台统一开发和管理调试项目提供了极大的便利,该会是一定程度上继续推动apple已经相当优秀的软件生态继续发展。

继续阅读