iOS SwiftUI 搜索栏和 REST-API
iOS SwiftUI Searchbar and REST-API
我正在尝试使用 SwiftUI 并希望使用搜索字符串从我的 REST API 中获取更新。
但是,我现在不确定如何将这两个组件组合在一起。
我希望你有一个想法。
这是我的代码:
struct ContentView: View {
@State private var searchTerm: String = ""
@ObservedObject var gameData: GameListViewModel = GameListViewModel(searchString: ### SEARCH STRING ???? ###)
var body: some View {
NavigationView{
Group{
// Games werden geladen...
if(self.gameData.isLoading) {
LoadingView()
}
// Games sind geladen:
else{
VStack{
// Suche:
searchBarView(text: self.$searchTerm)
// Ergebnisse:
List(self.gameData.games){ game in
NavigationLink(destination: GameDetailView(gameName: game.name ?? "0", gameId: 0)){
HStack {
VStack(alignment: .leading, spacing: 2) {
Text(game.name ?? "Kein Name gefunden")
.font(.headline)
Text("Cover: \(game.cover?.toString() ?? "0")")
.font(.subheadline)
.foregroundColor(.gray)
}
}
}
}
}
}
}
.navigationBarTitle(Text("Games"))
}
}
}
和搜索栏实现:
import Foundation
import SwiftUI
struct searchBarView: UIViewRepresentable {
@Binding var text:String
class Coordinator: NSObject, UISearchBarDelegate {
@Binding var text: String
init(text: Binding<String>){
_text = text
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
print(searchText)
text = searchText
}
}
func makeCoordinator() -> searchBarView.Coordinator {
return Coordinator(text: $text)
}
func makeUIView(context: UIViewRepresentableContext<searchBarView>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<searchBarView>) {
uiView.text = text
}
}
没有必要让 UIKit 参与进来,你可以像这样声明一个简单的搜索栏:
struct SearchBar: View {
@State var searchString: String = ""
var body: some View {
HStack {
TextField("Start typing",
text: $searchString,
onCommit: { self.performSearch() })
.textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: { self.performSearch() }) {
Image(systemName: "magnifyingglass")
}
} .padding()
}
func performSearch() {
}
}
然后将搜索逻辑放在 performSearch() 中。
搜索文本应该在视图模型中。
final class GameListViewModel: ObservableObject {
@Published var isLoading: Bool = false
@Published var games: [Game] = []
var searchTerm: String = ""
private let searchTappedSubject = PassthroughSubject<Void, Error>()
private var disposeBag = Set<AnyCancellable>()
init() {
searchTappedSubject
.flatMap {
self.requestGames(searchTerm: self.searchTerm)
.handleEvents(receiveSubscription: { _ in
DispatchQueue.main.async {
self.isLoading = true
}
},
receiveCompletion: { comp in
DispatchQueue.main.async {
self.isLoading = false
}
})
.eraseToAnyPublisher()
}
.replaceError(with: [])
.receive(on: DispatchQueue.main)
.assign(to: \.games, on: self)
.store(in: &disposeBag)
}
func onSearchTapped() {
searchTappedSubject.send(())
}
private func requestGames(searchTerm: String) -> AnyPublisher<[Game], Error> {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
return Fail(error: URLError(.badURL))
.mapError { [=10=] as Error }
.eraseToAnyPublisher()
}
return URLSession.shared.dataTaskPublisher(for: url)
.map { [=10=].data }
.mapError { [=10=] as Error }
.decode(type: [Game].self, decoder: JSONDecoder())
.map { searchTerm.isEmpty ? [=10=] : [=10=].filter { [=10=].title.contains(searchTerm) } }
.eraseToAnyPublisher()
}
}
每次调用 onSearchTapped
时,都会触发对新游戏的请求。
这里发生了很多事情 - 让我们从 requestGames
开始。
I'm using JSONPlaceholder free API to fetch some data and show it in the List.
requestGames
执行网络请求,从收到的 Data
解码 [Game]
。除此之外,返回的数组使用搜索字符串进行过滤(由于免费 API 限制 - 在现实世界场景中,您会在请求中使用查询参数 URL)。
现在让我们看一下视图模型构造函数。
事件的顺序是:
- 获取 "search tapped" 主题。
- 执行网络请求(
flatMap
)
- 在
flatMap
内部处理加载逻辑(在主队列上调度,因为 isLoading
在下面使用 Publisher
,如果值发布在后台线程)。
replaceError
将发布者的错误类型更改为 Never
,这是 assign
运算符的要求。
receiveOn
是必需的,因为由于网络请求,我们可能仍在后台队列中 - 我们想在主队列上发布结果。
assign
更新视图模型上的数组 games
。
store
将 Cancellable
保存在 disposeBag
这是查看代码(没有加载,为了演示):
struct ContentView: View {
@ObservedObject var viewModel = GameListViewModel()
var body: some View {
NavigationView {
Group {
VStack {
SearchBar(text: $viewModel.searchTerm,
onSearchButtonClicked: viewModel.onSearchTapped)
List(viewModel.games, id: \.title) { game in
Text(verbatim: game.title)
}
}
}
.navigationBarTitle(Text("Games"))
}
}
}
搜索栏实施:
struct SearchBar: UIViewRepresentable {
@Binding var text: String
var onSearchButtonClicked: (() -> Void)? = nil
class Coordinator: NSObject, UISearchBarDelegate {
let control: SearchBar
init(_ control: SearchBar) {
self.control = control
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
control.text = searchText
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
control.onSearchButtonClicked?()
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
uiView.text = text
}
}
我正在尝试使用 SwiftUI 并希望使用搜索字符串从我的 REST API 中获取更新。
但是,我现在不确定如何将这两个组件组合在一起。
我希望你有一个想法。
这是我的代码:
struct ContentView: View {
@State private var searchTerm: String = ""
@ObservedObject var gameData: GameListViewModel = GameListViewModel(searchString: ### SEARCH STRING ???? ###)
var body: some View {
NavigationView{
Group{
// Games werden geladen...
if(self.gameData.isLoading) {
LoadingView()
}
// Games sind geladen:
else{
VStack{
// Suche:
searchBarView(text: self.$searchTerm)
// Ergebnisse:
List(self.gameData.games){ game in
NavigationLink(destination: GameDetailView(gameName: game.name ?? "0", gameId: 0)){
HStack {
VStack(alignment: .leading, spacing: 2) {
Text(game.name ?? "Kein Name gefunden")
.font(.headline)
Text("Cover: \(game.cover?.toString() ?? "0")")
.font(.subheadline)
.foregroundColor(.gray)
}
}
}
}
}
}
}
.navigationBarTitle(Text("Games"))
}
}
}
和搜索栏实现:
import Foundation
import SwiftUI
struct searchBarView: UIViewRepresentable {
@Binding var text:String
class Coordinator: NSObject, UISearchBarDelegate {
@Binding var text: String
init(text: Binding<String>){
_text = text
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
print(searchText)
text = searchText
}
}
func makeCoordinator() -> searchBarView.Coordinator {
return Coordinator(text: $text)
}
func makeUIView(context: UIViewRepresentableContext<searchBarView>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<searchBarView>) {
uiView.text = text
}
}
没有必要让 UIKit 参与进来,你可以像这样声明一个简单的搜索栏:
struct SearchBar: View {
@State var searchString: String = ""
var body: some View {
HStack {
TextField("Start typing",
text: $searchString,
onCommit: { self.performSearch() })
.textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: { self.performSearch() }) {
Image(systemName: "magnifyingglass")
}
} .padding()
}
func performSearch() {
}
}
然后将搜索逻辑放在 performSearch() 中。
搜索文本应该在视图模型中。
final class GameListViewModel: ObservableObject {
@Published var isLoading: Bool = false
@Published var games: [Game] = []
var searchTerm: String = ""
private let searchTappedSubject = PassthroughSubject<Void, Error>()
private var disposeBag = Set<AnyCancellable>()
init() {
searchTappedSubject
.flatMap {
self.requestGames(searchTerm: self.searchTerm)
.handleEvents(receiveSubscription: { _ in
DispatchQueue.main.async {
self.isLoading = true
}
},
receiveCompletion: { comp in
DispatchQueue.main.async {
self.isLoading = false
}
})
.eraseToAnyPublisher()
}
.replaceError(with: [])
.receive(on: DispatchQueue.main)
.assign(to: \.games, on: self)
.store(in: &disposeBag)
}
func onSearchTapped() {
searchTappedSubject.send(())
}
private func requestGames(searchTerm: String) -> AnyPublisher<[Game], Error> {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
return Fail(error: URLError(.badURL))
.mapError { [=10=] as Error }
.eraseToAnyPublisher()
}
return URLSession.shared.dataTaskPublisher(for: url)
.map { [=10=].data }
.mapError { [=10=] as Error }
.decode(type: [Game].self, decoder: JSONDecoder())
.map { searchTerm.isEmpty ? [=10=] : [=10=].filter { [=10=].title.contains(searchTerm) } }
.eraseToAnyPublisher()
}
}
每次调用 onSearchTapped
时,都会触发对新游戏的请求。
这里发生了很多事情 - 让我们从 requestGames
开始。
I'm using JSONPlaceholder free API to fetch some data and show it in the List.
requestGames
执行网络请求,从收到的 Data
解码 [Game]
。除此之外,返回的数组使用搜索字符串进行过滤(由于免费 API 限制 - 在现实世界场景中,您会在请求中使用查询参数 URL)。
现在让我们看一下视图模型构造函数。
事件的顺序是:
- 获取 "search tapped" 主题。
- 执行网络请求(
flatMap
) - 在
flatMap
内部处理加载逻辑(在主队列上调度,因为isLoading
在下面使用Publisher
,如果值发布在后台线程)。 replaceError
将发布者的错误类型更改为Never
,这是assign
运算符的要求。receiveOn
是必需的,因为由于网络请求,我们可能仍在后台队列中 - 我们想在主队列上发布结果。assign
更新视图模型上的数组games
。store
将Cancellable
保存在disposeBag
这是查看代码(没有加载,为了演示):
struct ContentView: View {
@ObservedObject var viewModel = GameListViewModel()
var body: some View {
NavigationView {
Group {
VStack {
SearchBar(text: $viewModel.searchTerm,
onSearchButtonClicked: viewModel.onSearchTapped)
List(viewModel.games, id: \.title) { game in
Text(verbatim: game.title)
}
}
}
.navigationBarTitle(Text("Games"))
}
}
}
搜索栏实施:
struct SearchBar: UIViewRepresentable {
@Binding var text: String
var onSearchButtonClicked: (() -> Void)? = nil
class Coordinator: NSObject, UISearchBarDelegate {
let control: SearchBar
init(_ control: SearchBar) {
self.control = control
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
control.text = searchText
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
control.onSearchButtonClicked?()
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
uiView.text = text
}
}