SwiftUI iOS 精品项目之 04 RSS阅读器基于Json和CoreData (项目含源码)

实战需求

SwiftUI iOS 精品项目之RSS阅读器基于Json和CoreData (项目含源码)

RSS阅读器是适用于iOS的最小,最简单的RSS阅读器,具有使用SwiftUI和CoreData的功能和简化的设计

本文价值与收获

看完本文后,您将能够作出下面的界面

RSS阅读器

RSS阅读器

核心组件库

  • AFNetworking
    AFNetworking是一个适用于iOS,macOS,watchOS和tvOS的令人愉悦的网络库。它基[Foundation URL Loading System构建,扩展了Cocoa中内置的强大的高级网络抽象。它具有模块化的体系结构,以及精心设计的,功能丰富的API,使用起来很愉快。AFNetworking可为iPhone,iPad和Mac上的某些最受欢迎和广受好评的应用程序提供支持。

  • BetterSafariView
    在SwiftUI中呈现SFSafariViewController或启动ASWebAuthenticationSession的更好方法。

  • FaviconFinder
    FaviconFinder是一个很小的纯Swift库,专为iOS和macOS应用程序设计,可让您检测网站使用的收藏夹图标。 FaviconFinder会为您处理麻烦的工作,并遍历Favicon可能位于的多个位置,并在找到图像后将图像简单地传递给您。

  • FeedKit
    建立指向RSS,Atom或JSON Feed的URL。

    let feedURL = URL(string: "http://images.apple.com/main/rss/hotnews/hotnews.rss")!
  • Introspect
    Introspect允许您获取SwiftUI视图的基础UIKit或AppKit元素。例如,使用Introspect,您可以访问UITableView来修改分隔符,或者访问UINavigationController来自定义标签栏。

    List {
    Text("Item 1")
    Text("Item 2")
    }
    .introspectTableView { tableView in
    tableView.separatorStyle = .none
    }
  • Kingfisher
    从url下载图像,将其发送到内存缓存和磁盘缓存,并在imageView中显示。以后使用相同的URL设置时,将从缓存中检索图像并立即显示。

import KingfisherSwiftUI

var body: some View {
    KFImage(URL(string: "https://example.com/image.png")!)
}
  • Reachability
    Reachability.swift替代了Apple的Reachability示例,该示例在Swift中使用闭包进行了重写。

  • SDWebImage
    该库提供了具有缓存支持的异步图像下载器。为了方便起见,我们为UI元素添加了类别,例如UIImageView,UIButton,MKAnnotationView。

  • SDWebImageSwiftUI
    SDWebImageSwiftUI是一个SwiftUI图像加载框架,基于SDWebImage它带来了SDWebImage的所有您喜欢的功能,例如异步图像加载,内存/磁盘缓存,动画图像回放和性能。该框架提供了不同的View结构,这些API与SwiftUI框架准则相匹配。如果您熟悉Image,将会发现使用WebImageAnimatedImage很容易。

  • SwiftSoup
    SwiftSoup是一个纯跨平台的Swift库(macOS,iOS,tvOS,watchOS和Linux!),用于处理实际的HTML。它使用DOM,CSS和类似jQuery的最佳方法,为提取和处理数据提供了非常方便的API。实现了WHATWG HTML5规范,并将HTML解析为与现代浏览器相同的DOM。

  • SwipeCell
    SwipeCell 是一个用Swift 5.3开发的 SwiftUI库.目标是为了实现类似iOS Mail程序实现的左右滑动菜单功能.
    SwipeCell 需要 XCode 12 ,iOS 14

  • SwipeCellKit
    可滑动的UITableViewCellUICollectionViewCell,支持:左右滑动动作,具有以下功能的操作按钮:*仅文本,文本+图片,仅图片,触觉反馈

  • Valet
    Valet使您可以安全地将数据存储在iOS,tvOS,watchOS或macOS钥匙串中,而无需了解钥匙串的工作原理。这简单。我们承诺。

    看完本文您将掌握的技能


基础知识

  • 模型管理

    @ObservedObject var viewModel: RSSListViewModel
    @ObservedObject var searchBar: SearchBar = SearchBar()
    @StateObject var rssFeedViewModel: RSSFeedViewModel
    @StateObject var archiveListViewModel: ArchiveListViewModel
  • 消息发送

    private let addRSSPublisher = NotificationCenter.default.publisher(for: Notification.Name.init("addNewRSSPublisher"))
    private let rssRefreshPublisher = NotificationCenter.default.publisher(for: Notification.Name.init("rssListNeedRefresh"))
  • 网络管理Reachability.swift

    public class Reachability {
    
    public typealias NetworkReachable = (Reachability) -> ()
    public typealias NetworkUnreachable = (Reachability) -> ()
    
    @available(*, unavailable, renamed: "Connection")
    public enum NetworkStatus: CustomStringConvertible {
        case notReachable, reachableViaWiFi, reachableViaWWAN
        public var description: String {
            switch self {
            case .reachableViaWWAN: return "Cellular"
            case .reachableViaWiFi: return "WiFi"
            case .notReachable: return "No Connection"
            }
        }
    }
    }
  • 扩展Binding

import Foundation
import SwiftUI

public extension Binding {
    func didSet(_ didSet: @escaping (Value) -> Void) -> Binding<Value> {
        Binding(
            get: { wrappedValue },
            set: { newValue in
                self.wrappedValue = newValue
                didSet(newValue)
            }
        )
    }
}
  • 扩展Color
import SwiftUI
import Foundation

extension Color {
    init(_ rgb: UInt, _ alpha: CGFloat = 1.0) {
        self.init(RGBColorSpace.sRGB,
              red: Double((rgb & 0xFF0000) >> 16) / 255.0,
              green: Double((rgb & 0x00FF00) >> 8) / 255.0,
              blue: Double(rgb & 0x0000FF) / 255.0, opacity: Double(alpha))
    }
}
  • 扩展Image快速

import SwiftUI
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public extension SwiftUI.Image {
    func styleFit() -> some View {
        return self
            .resizable()
            .aspectRatio(contentMode: .fit)
    }
}
  • View拓展
import SwiftUI

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public extension View {
    func erase() -> AnyView {
        return AnyView(self)
    }

    @ViewBuilder
    func applyIf<T: View>(_ condition: @autoclosure () -> Bool, apply: (Self) -> T) -> some View {
        if condition() {
            apply(self)
        } else {
            self
        }
    }

    @ViewBuilder
    func hidden(_ hides: Bool) -> some View {
        switch hides {
        case true: self.hidden()
        case false: self
        }
    }
}
  • ShakeEffect效果
#if canImport(SwiftUI)

import SwiftUI

// https://learntalks.com/FrenchKit/2019/FrenchKit-2019-Animations-with-SwiftUI-Chris-Eidhof/

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct ShakeEffect: GeometryEffect {
    var position: CGFloat = 0

    public var animatableData: CGFloat {
        get { position }
        set { position = newValue }
    }

    public func effectValue(size: CGSize) -> ProjectionTransform {
        return ProjectionTransform(
            CGAffineTransform(translationX: sin(position * 2 * .pi), y: 0)
        )
    }
}

#endif
  • 封装UITextView
import SwiftUI

struct TextView: UIViewRepresentable {

    @Binding var text: String
    @Binding var textStyle: UIFont.TextStyle

    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()
        textView.font = UIFont.preferredFont(forTextStyle: textStyle)
        textView.autocapitalizationType = .sentences
        textView.isSelectable = true
        textView.isUserInteractionEnabled = true
        textView.isEditable = true
        return textView
    }

    func updateUIView(_ uiView: UITextView, context: Context) {
        uiView.text = text
        uiView.font = UIFont.preferredFont(forTextStyle: textStyle)
    }

    static func dismantleUIView(_ uiView: UITextView, coordinator: Coordinator) {

    }
}

struct TextView_Previews: PreviewProvider {
    static var previews: some View {
        TextView(text: .constant("TextView Previews"), textStyle: .constant(.title3))
    }
}
  • SearchBar 组件
import SwiftUI

class SearchBar: NSObject, ObservableObject {
    @Published var text: String = ""
    let searchController: UISearchController = UISearchController(searchResultsController: nil)

    override init() {
        super.init()
        self.searchController.obscuresBackgroundDuringPresentation = false
        self.searchController.searchResultsUpdater = self
        self.searchController.searchBar.autocapitalizationType = .none
        self.searchController.searchBar.placeholder = "Search"
    }
}

extension SearchBar: UISearchResultsUpdating {

    func updateSearchResults(for searchController: UISearchController) {

        // Publish search bar text changes.
        if let searchBarText = searchController.searchBar.text {
            self.text = searchBarText
        }
    }
}

struct SearchBarModifier: ViewModifier {
    let searchBar: SearchBar

    func body(content: Content) -> some View {
        content
            .overlay(
                ViewControllerResolver { viewController in
                    viewController.navigationItem.searchController = self.searchBar.searchController
                }
                .frame(width: 0, height: 0)
            )
    }
}

extension View {
    func add(_ searchBar: SearchBar) -> some View {
        return self.modifier(SearchBarModifier(searchBar: searchBar))
    }
}

final class ViewControllerResolver: UIViewControllerRepresentable {
    let onResolve: (UIViewController) -> Void

    init(onResolve: @escaping (UIViewController) -> Void) {
        self.onResolve = onResolve
    }

    func makeUIViewController(context: Context) -> ParentResolverViewController {
        ParentResolverViewController(onResolve: onResolve)
    }

    func updateUIViewController(_ uiViewController: ParentResolverViewController, context: Context) {

    }
}

class ParentResolverViewController: UIViewController {

    let onResolve: (UIViewController) -> Void

    init(onResolve: @escaping (UIViewController) -> Void) {
        self.onResolve = onResolve
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("Use init(onResolve:) to instantiate ParentResolverViewController.")
    }

    override func didMove(toParent parent: UIViewController?) {
        super.didMove(toParent: parent)

        if let parent = parent {
            onResolve(parent)
        }
    }
}

struct Searchbar: View {
    @ObservedObject var searchBar: SearchBar = SearchBar()

    var body: some View {
        NavigationView{
            List {
                Text("test")
                Text("hi")
                Text("hello")
            }
            .navigationBarTitle("Search")
            .listStyle(InsetGroupedListStyle())
            .add(searchBar)
        }
    }
}

struct Searchbar_Previews: PreviewProvider {
    static var previews: some View {
        Searchbar()
    }
}
  • 封装WKWebview

    struct WKWebViewWrapper: UIViewRepresentable {
    
    class Coordinator: NSObject, WKNavigationDelegate, UIScrollViewDelegate {
        private var viewModel: WKWebViewModel
    
        init(_ viewModel: WKWebViewModel) {
            self.viewModel = viewModel
        }
    
        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            let total = Double(webView.scrollView.contentSize.height)
            self.viewModel.didFinishLoading = true
            self.viewModel.canGoBack = webView.canGoBack
            self.viewModel.canGoForward = webView.canGoForward
            self.viewModel.total = total
    
            if self.viewModel.isFirst {
                DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3) {
                    var contentOffsetY = 0.0
                    if self.viewModel.progress > 0 {
                        contentOffsetY = total * self.viewModel.progress - Double(webView.scrollView.bounds.height)
                    }
                    webView.scrollView.setContentOffset(CGPoint(x: 0, y: contentOffsetY), animated: false)
                }
            }
        }
    
        func scrollViewDidScroll(_ scrollView: UIScrollView) {
            let contentOffsetY = scrollView.contentOffset.y + scrollView.frame.height
            self.viewModel.apply(progress: Double(contentOffsetY))
        }
    }
    
    @ObservedObject var viewModel: WKWebViewModel
    
    let webView = WKWebView()
    
    func makeUIView(context: Context) -> WKWebView {
        self.webView.navigationDelegate = context.coordinator
        self.webView.scrollView.delegate = context.coordinator
        if let url = URL(string: viewModel.link) {
            self.webView.load(URLRequest(url: url))
        }
        return self.webView
    }
    
    func updateUIView(_ uiView: WKWebView, context: Context) {
    
    }
    
    static func dismantleUIView(_ uiView: WKWebView, coordinator: Coordinator) {
    
    }
    
    func makeCoordinator() -> WKWebViewWrapper.Coordinator {
        return Coordinator(viewModel)
    }
    }
  • 封装SFSafariViewController

import SwiftUI
import SafariServices

struct SafariView: UIViewControllerRepresentable {

    let url: URL

    func makeUIViewController(context: UIViewControllerRepresentableContext<SafariView>) -> SFSafariViewController {
        let config = SFSafariViewController.Configuration()
        config.entersReaderIfAvailable = true
        return SFSafariViewController(url: url, configuration: config)
    }

    func updateUIViewController(_ uiViewController: SFSafariViewController, context: UIViewControllerRepresentableContext<SafariView>) {

    }
}

#if DEBUG

struct SafariView_Previews: PreviewProvider {
    static var previews: some View {
        SafariView(url: URL(string: "https://www.github.com")!)
    }
}

#endif
  • FilterBar 过滤bar
import SwiftUI
import Combine
import FeedKit
import Foundation

enum FilterType: String {
    case all = "All"
    case unreadIsOn = "Unread"
    case isArchive = "Starred"
}

struct FilterBar: View {
    @Binding var selectedFilter: FilterType
//    @Binding var showFilter: Bool
    @Binding var isOn: Bool
    var markedAllPostsRead: (() -> Void)?

    var body: some View {
        ZStack {
//            Capsule()
            RoundedRectangle(cornerRadius: 25.0)
                .frame(width: 205, height: 35).foregroundColor(Color("text")).opacity(0.1)
            HStack(spacing: 0) {
                Spacer()
                ZStack {
                    if selectedFilter == .isArchive {
                        Capsule()
                            .frame(width: 85, height: 25)
                            .opacity(0.5)
                            .foregroundColor(Color.gray.opacity(0.5))

                    HStack {
                        Image(systemName: "star.fill").font(.system(size: 10, weight: .black))
                        Text(FilterType.isArchive.rawValue)
                            .textCase(.uppercase)
                            .font(.system(size: 10, weight: .medium, design: .rounded))
                            .foregroundColor(Color("text"))
                        }
                    } else {
                        Image(systemName: "star.fill").font(.system(size: 10, weight: .black))
                    }
                }
                .padding()
                .onTapGesture {
                    self.selectedFilter = .isArchive
                }
                Divider()

                ZStack {
                    if selectedFilter == .unreadIsOn {
                        Capsule()
                            .frame(width: 85, height: 25)
                            .opacity(0.5)
                            .foregroundColor(Color.gray.opacity(0.5))
                    HStack {
                        Image(systemName: "circle.fill").font(.system(size: 10, weight: .black))
                        Text(FilterType.unreadIsOn.rawValue)
                            .textCase(.uppercase)
                            .font(.system(size: 10, weight: .medium, design: .rounded))
                                .foregroundColor(Color("text"))
                        }
                    } else {
                        Image(systemName: "circle.fill").font(.system(size: 10, weight: .black)).padding()
                    }
                }//.padding()
                .onTapGesture {
                    self.selectedFilter = .unreadIsOn
                }
                Divider()

                ZStack {
                    if selectedFilter == .all {
                        Capsule()
                            .frame(width: 65, height: 25)
                            .opacity(0.5)
                            .foregroundColor(Color.gray.opacity(0.5))

                    HStack{
                        Image(systemName: "text.justifyleft").font(.system(size: 10, weight: .black))
                        Text(FilterType.all.rawValue)
                            .textCase(.uppercase)
                            .font(.system(size: 10, weight: .medium, design: .rounded))
                            .foregroundColor(Color("text"))
                        }
                    } else {
                        Image(systemName: "text.justifyleft").font(.system(size: 10, weight: .black))
                        }
                    }.padding()
                    .onTapGesture {
                        self.selectedFilter = .all
                    }
                    Spacer()
            }
            .frame(width: 125, height: 20)
        }
    }
}

全文地址

CSDN:

小专栏:


加入我们一起学习SwiftUI

QQ:3365059189
SwiftUI技术交流QQ群:518696470
教程网站:www.openswiftui.com

发表回复