frame越过另一个frame_SwiftUI 从零开始做一个少数派客户端

在学习Swift 2.0 正式来临之前,我决心先体验一下使用 SwiftUI 写个小软件。本文基于目前的 SwiftUI 开发并没有使用任何2.0的新特性。

熟悉基本操作

4816f82dfed02c711fa2720c6ebbb5be.png

首先多出来这个界面叫做 Canvas 不小心关掉的话可以在 Editor > Canvas 打开

左下角的 可以在切换文件固定这个界面,很方便的功能

右下角是很普通的缩放功能 右侧有两个按钮▶️可以让 preview 运行起来, 可以让 preview 之间在设备上运行

效果图

3af7ca9dce1cf6ec0b5acdec0e00c42c.png

App 整体上就是一个文章列表,每个文章点进去就是一个 web view,所以组件就三个:

  • List 文章列表
  • Cell 列表元素显示-图片,标题和副标题
  • WebView 网页-文章内容

数据模型

根据 API 接口 https://sspai.com/api/v1/article/index/page/get 返回的数据创建数据模型

struct NetworkResponse<T: Codable> : Codable {
    let msg: String
    let error: Int
    let data: T
}
struct ItemBean: Codable, Identifiable {
    let id: Int
    let title: String
    let summary: String
    let banner: String
    //... more
}
List组件需要 item 满足 Identifiable 协议

List

创建文件ItemList.swift

struct ItemList: View {
    @State var items: [ItemBean] = []
    var body: some View {
        List(0 ..< self.items.count, id: .self) { index in
            Text(self.items[index].title)
        }.onAppear {
            self.fetchItemList()
        }
    }

    func fetchItemList() {
        let url = URL(string: "https://sspai.com/api/v1/article/index/page/get")!
        URLSession.shared.dataTask(with: url) { (data, _, _) in
            guard let data = data else {
                // Error
                return
            }
            guard let items = try? JSONDecoder().decode(NetworkResponse<[ItemBean]>.self, from: data).data else {
                // Error
                return
            }
            DispatchQueue.main.async {
                self.items = items
            }
        }.resume()
    }
}
@State修饰符可以关联View的状态,当item变化会重新创建视图 self.items = items这行代码会刷新界面需要放到DispatchQueue.main

Cell

Cell就很普通,后面在加图片。

struct ItemCell: View {
    let item: ItemBean
    var body: some View {
        VStack {
            Text("https://cdn.sspai.com/" + item.banner)
            Text(item.title)
                .font(.headline)
            Spacer()
                .frame(height: 8)
            Text(item.summary)
                .font(.subheadline)
                .foregroundColor(Color.secondary)
        }
    }
}

WebView

SwiftUI的组件中是没有WebView的,但是提供了UIViewRepresentable来使用UIKit中的组件,只需要实现协议即可。

struct WebView: UIViewRepresentable {
    let urlString: String
    func makeUIView(context: Context) -> WKWebView {
        guard let url = URL(string: self.urlString) else {
            return WKWebView()
        }
        let requeset = URLRequest(url: url)
        let wk = WKWebView()
        wk.isOpaque = false
        wk.load(requeset)
        return wk
    }
    func updateUIView(_ uiView: WKWebView, context: Context) {}
}

导航

把列表放到 NavigationView 中,Cell 放到 NavigationLink 中。

如果要改导航栏,在NavigationView里面的view改 直接使用NavigationLink会有一个箭头,可以放到ZStack中 导航出来的有SafeArea,如果想要全屏显示可以用 edgesIgnoringSafeArea(Edge.Set.all)
NavigationView {
    List(0 ..< self.item.count, id: .self) { index in
        ZStack {
            ItemCell(item: self.item[index])
            NavigationLink(destination: WebView(urlString: "https://sspai.com/post/(self.item[index].id)" ).navigationBarTitle("",displayMode: .inline)
                .edgesIgnoringSafeArea(Edge.Set.all)
            ) {
                EmptyView()
            }

        }
    }.onAppear {
        self.fetchItemList()
    }.navigationBarTitle("sspai")
}

网络图片

SwiftUI 目前还没有支持简单网络图片加载,不过有特别好用的第三方库,也可以自己写一个简单的用。不过还是建议使用成熟的第三方解决方案。

struct NetWorkImage: View {
    init(url: URL) {
        self.imageLoader = Loader(url)
    }

    @ObservedObject private var imageLoader: Loader
    var image: UIImage? {
        imageLoader.data.flatMap(UIImage.init)
    }

    var body: some View {
        VStack {
            if image != nil  {
                Image(uiImage: image!)
                    .resizable()
            } else {
                EmptyView()
            }
        }
    }

}

final class Loader: ObservableObject {

    var task: URLSessionDataTask!
    @Published var data: Data? = nil

    init(_ url: URL) {
        task = URLSession.shared.dataTask(with: url, completionHandler: { data, _, _ in
            DispatchQueue.main.async {
                self.data = data
            }
        })
        task.resume()
    }
    deinit {
        task.cancel()
    }
}

改造之前的 Cell,让它显示图片

struct ItemCell: View {
    let item: ItemBean
    var body: some View {
        VStack {
            GeometryReader { geometry in
                NetWorkImage(url:URL(string: "https://cdn.sspai.com/(self.item.banner)")!)
                    .scaledToFill()
                    .frame(width: geometry.size.width, height: geometry.size.width/2)
                    .clipShape(RoundedRectangle(cornerRadius: 16))
            }
            .aspectRatio(2, contentMode: .fit)
            .clipped()
            Text(item.title)
                .font(.headline)
            Spacer()
                .frame(height: 8)
            Text(item.summary)
                .font(.subheadline)
                .foregroundColor(Color.secondary)
        }
    }
}

刷新 加载更多

下拉刷新暂时没有比较简单的方案,我就直接导航上加一个刷新按钮。 加载更多就比较简单,给 Cell 加onAppear {self.fetchMoreItemsIfNeed(current: index)}根据index判断是否加载更多。ItemList最终代码就设这样的:

struct ItemList: View {
    @State var items: [ItemBean] = []
    var body: some View {
        NavigationView {
            List(0 ..< self.items.count, id: .self) { index in
                ZStack {
                    ItemCell(item: self.items[index])
                        .onAppear {
                            self.fetchMoreItemsIfNeed(current: index)
                    }
                    NavigationLink(destination: WebView(urlString: "https://sspai.com/post/(self.items[index].id)" ).navigationBarTitle("",displayMode: .inline)
                        .edgesIgnoringSafeArea(Edge.Set.all)
                    ) {
                        EmptyView()
                    }

                }
            }.onAppear {
                self.fetchItemList()
            }
            .navigationBarTitle("sspai")
            .navigationBarItems(trailing: Button(action: {
                self.fetchItemList()
            }, label: {
                Text("刷新")
            }))
        }
    }

    func fetchItemList() {
        let url = URL(string: "https://sspai.com/api/v1/article/index/page/get")!
        URLSession.shared.dataTask(with: url) { (data, _, _) in
            guard let data = data else {
                // Error
                return
            }
            guard let items = try? JSONDecoder().decode(NetworkResponse<[ItemBean]>.self, from: data).data else {
                // Error
                return
            }
            DispatchQueue.main.async {
                self.items = items
            }
        }.resume()
    }

    func fetchMoreItemsIfNeed(current: Int) {
        guard current == self.items.count - 1 else {
            return
        }
        let url = URL(string: "https://sspai.com/api/v1/article/index/page/get?limit=10&offset=(items.count)")!

        URLSession.shared.dataTask(with: url) { (data, response, error) in
            DispatchQueue.main.async {
                guard let data = data else {
                    // Error
                    return
                }
                guard let items = try? JSONDecoder().decode(NetworkResponse<[ItemBean]>.self, from: data).data else {
                    // Error
                    return
                }
                self.items.append(contentsOf: items)
            }

        }.resume()
    }
}

总结

这是我第一个使用 SwiftUI 完成的 App,支持 iOS 和 Mac。熟悉 Flutter 的我用 SwiftUI 刚开始的感到很难受,Apple 的工具链还非常不完善,不过熟悉的之后会好一些,不过还是希望下个版本有所改进吧。

  • 缺点
    • 在布局的时候不要妄图让Xcode告诉你改怎么写,代码提示真的真的真的会让人崩溃。
    • 调试的时候要不要太在意错误提示的信息,真的没用。
    • 文档很简陋,也没有示例代码,全靠 Google
  • 优点
    • 调试界面不用运行整个项目,改了界面也能马上生效,我觉得比 Flutter 还好用
    • SwiftUI 代码很直观
Github​github.com
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值