实战需求
SwiftUI iOS 精品项目之RSS阅读器基于Json和CoreData (项目含源码)
RSS阅读器是适用于iOS的最小,最简单的RSS阅读器,具有使用SwiftUI和CoreData的功能和简化的设计
本文价值与收获
看完本文后,您将能够作出下面的界面
核心组件库
-
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
,将会发现使用WebImage
和AnimatedImage
很容易。 -
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
可滑动的UITableViewCell
或UICollectionViewCell
,支持:左右滑动动作,具有以下功能的操作按钮:*仅文本,文本+图片,仅图片,触觉反馈 -
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