Binding value changes only once - swift

I have the following View structure and run into the problem of the button working the first time I click and disabling the button like it should, but once I clicked one button it does not work for the other buttons. The Item is always the right one, I checked that by printing it out.
My ReelsView:
struct ReelsView: View {
#State var currentReel = ""
#State var items: [Item] = [
Item(chance: "1:1m", tradeIn: 2000, name: "Apple Watch", price: "$200", rarityType: "common", description: "The Apple Watch", reel: Reel(player: AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: "apple-watch", ofType: "mp4") ?? "")), bid: false)),
Item(chance: "1:20m", tradeIn: 27500, name: "Ibiza vacation", price: "$2750,00", rarityType: "superRare", description: "Such a wonderful place for a vacation", reel: Reel(player: AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: "ibiza", ofType: "mp4") ?? "")), bid: false)),
]
var body: some View {
TabView(selection: $currentReel) {
ForEach($items) { $reel in
ReelsPlayer(reel: $reel.reel, currentReel: $currentReel, item: $reel)
.tag(reel.reel.id)
}
}
}
}
My ReelsPlayer:
struct ReelsPlayer: View {
#Binding var reel: Reel
#Binding var currentReel: String
#Binding var item: Item
var body: some View {
ZStack {
if let player = reel.player {
CustomVideoPlayer(player: player)
.allowsHitTesting(false)
}
}
.overlay {
BottomOverlay(item: $item)
.allowsHitTesting(true)
}
}
}
My BottomOverlay:
struct BottomOverlay: View {
#Binding var item: Item
var body: some View {
Button() {
item.reel.bid.toggle()
print("item: ", item)
print("item: ", $item)
} label: {
Text(item.reel.bid ? "Already Bid" : "Bid")
}
}
}
struct Reel: Identifiable {
var id = UUID().uuidString
var player: AVPlayer
var bid: Bool
}
struct Item: Identifiable, Hashable {
static func == (lhs: Item, rhs: Item) -> Bool {
lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(chance)
hasher.combine(name)
hasher.combine(price)
hasher.combine(tradeIn)
hasher.combine(rarityType)
hasher.combine(description)
}
var id: String = UUID().uuidString
var chance: String
var tradeIn: Int
var name: String
var price: String
var rarityType: String
var description: String
var reel: Reel
}

Here is the code I used in my test, to show that the two buttons acts separately. Click on one, and it print the item id and state. Click on the other, and same for that item. If you un-comment .disabled(item.clicked), then it only works once, because the Button (for that item) is now disabled.
struct Item: Identifiable {
let id = UUID()
var name: String
var clicked: Bool
}
struct ContentView: View {
var body: some View {
MainView()
}
}
struct MainView: View {
#State var items: [Item] = [Item(name: "item1", clicked: false),Item(name: "item2", clicked: false)]
var body: some View {
ForEach($items) { $item in
ItemView(item: $item)
}
}
}
struct ItemView: View {
#Binding var item: Item
var body: some View {
VStack {
Color.green
}.overlay {
OverlayView(item: $item)
}
}
}
struct OverlayView: View {
#Binding var item: Item
var body: some View {
VStack (spacing: 33){
Button() {
item.clicked.toggle()
print("--> item.id: \(item) item.clicked: \(item.clicked)")
} label: {
Text(item.name)
}//.disabled(item.clicked)
// Button() {
// item.clicked.toggle()
// } label: {
// Text("enable again")
// }
}
}
}
EDIT-1: in view of your new code. Try this example code, works well for me
import Foundation
import SwiftUI
import UIKit
import AVFoundation
import AVKit
struct ContentView: View {
var body: some View {
ReelsView()
}
}
struct ReelsView: View {
#State var currentReel = ""
#State var items: [Item] = [
Item(chance: "1:1m", tradeIn: 2000, name: "Apple Watch", price: "$200", rarityType: "common", description: "The Apple Watch", reel: Reel(player: AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: "apple-watch", ofType: "mp4") ?? "")), bid: false)),
Item(chance: "1:20m", tradeIn: 27500, name: "Ibiza vacation", price: "$2750,00", rarityType: "superRare", description: "Such a wonderful place for a vacation", reel: Reel(player: AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: "ibiza", ofType: "mp4") ?? "")), bid: false)),
]
var body: some View {
TabView(selection: $currentReel) {
ForEach($items) { $item in
ReelsPlayer(currentReel: $currentReel, item: $item) // <-- here
.tag(item.reel.id)
}
}.tabViewStyle(.page) // <-- here
}
}
struct ReelsPlayer: View {
#Binding var currentReel: String
#Binding var item: Item // <-- here
var body: some View {
ZStack {
if let player = item.reel.player { // <-- here
// CustomVideoPlayer(player: player)
// for testing
VStack {
if item.name == "Apple Watch" { Color.yellow } else { Color.green }
}
.allowsHitTesting(false)
}
}
.overlay {
BottomOverlay(item: $item)
.allowsHitTesting(true)
}
}
}
struct BottomOverlay: View {
#Binding var item: Item
var body: some View {
Button() {
item.reel.bid.toggle()
print("----> BottomOverlay item.reel.bid: ", item.reel.bid) // <-- here
} label: {
Text(item.reel.bid ? "Already Bid" : "Bid")
}
}
}
struct Reel: Identifiable, Hashable { // <-- here
var id = UUID().uuidString
var player: AVPlayer
var bid: Bool
}
struct Item: Identifiable, Hashable { // <-- here
var id: String = UUID().uuidString
var chance: String
var tradeIn: Int
var name: String
var price: String
var rarityType: String
var description: String
var reel: Reel
}

Related

SwiftUI: How to get notified when a field in a singleton object get changed?

class SharedData: ObservableObject {
static let shared = SharedData()
#Published var sharedState = SharedState()
}
struct SharedState {
var allMMS: [MMS] = []
var typeTrees: [TTMaker] = []
var sampleInputs: [String: String] = [:]
var selectedTypeTreeName: String?
var selectedMMSPathName: String?
var maps: [String: FunctionalMap] = [:]
var mapId: String?
var selectedMenuItem: String? = nil
}
struct ContentView: View {
#ObservedObject var sharedData = SharedData.shared
set a file / string on the view:
SharedData.shared.sharedState.typeTrees.append(ttMaker)
I would expect the List in the same would get updated:
List {
ForEach(SharedData.shared.sharedState.typeTrees, id: \.self) { tree in
Button(action: {
SharedData.shared.sharedState.selectedTypeTreeName = tree.newTree.filename
}) {
HStack {
Text(tree.newTree.filename)
Spacer()
if tree.newTree.filename == SharedData.shared.sharedState.selectedTypeTreeName {
Image(systemName: "checkmark")
}
}
}
}
}
Is it any similar oslution than context in React?

SwiftUI: #State value doesn't update after async network request

My aim is the change data in DetailView(). Normally in this case I'm using #State + #Binding and it's works fine with static data, but when I trying to update ViewModel with data from network request I'm loosing functionality of #State (new data doesn't passing to #State value it's stays empty). I checked network request and decoding process - everything ok with it. Sorry my code example a bit long but it's the shortest way that I found to recreate the problem...
Models:
struct LeagueResponse: Decodable {
var status: Bool?
var data: [League] = []
}
struct League: Codable, Identifiable {
let id: String
let name: String
var seasons: [Season]?
}
struct SeasonResponse: Codable {
var status: Bool?
var data: LeagueData?
}
struct LeagueData: Codable {
let name: String?
let desc: String
let abbreviation: String?
let seasons: [Season]
}
struct Season: Codable {
let year: Int
let displayName: String
}
ViewModel:
class LeagueViewModel: ObservableObject {
#Published var leagues: [League] = []
init() {
Task {
try await getLeagueData()
}
}
private func getLeagueData() async throws {
let (data, _) = try await URLSession.shared.data(from: URL(string: "https://api-football-standings.azharimm.site/leagues")!)
guard let leagues = try? JSONDecoder().decode(LeagueResponse.self, from: data) else {
throw URLError(.cannotParseResponse)
}
await MainActor.run {
self.leagues = leagues.data
}
}
func loadSeasons(forLeague id: String) async throws {
let (data, _) = try await URLSession.shared.data(from: URL(string: "https://api-football-standings.azharimm.site/leagues/\(id)/seasons")!)
guard let seasons = try? JSONDecoder().decode(SeasonResponse.self, from: data) else {
throw URLError(.cannotParseResponse)
}
await MainActor.run {
if let responsedLeagueIndex = leagues.firstIndex(where: { $0.id == id }),
let unwrappedSeasons = seasons.data?.seasons {
leagues[responsedLeagueIndex].seasons = unwrappedSeasons
print(unwrappedSeasons) // successfully getting and parsing data
}
}
}
}
Views:
struct ContentView: View {
#StateObject var vm = LeagueViewModel()
var body: some View {
NavigationView {
VStack {
if vm.leagues.isEmpty {
ProgressView()
} else {
List {
ForEach(vm.leagues) { league in
NavigationLink(destination: DetailView(league: league)) {
Text(league.name)
}
}
}
}
}
.navigationBarTitle(Text("Leagues"), displayMode: .large)
}
.environmentObject(vm)
}
}
struct DetailView: View {
#EnvironmentObject var vm: LeagueViewModel
#State var league: League
var body: some View {
VStack {
if let unwrappedSeasons = league.seasons {
List {
ForEach(unwrappedSeasons, id: \.year) { season in
Text(season.displayName)
}
}
} else {
ProgressView()
}
}
.onAppear {
Task {
try await vm.loadSeasons(forLeague: league.id)
}
}
.navigationBarTitle(Text("League Detail"), displayMode: .inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
ChangeButton(selectedLeague: $league)
}
}
}
}
struct ChangeButton: View {
#EnvironmentObject var vm: LeagueViewModel
#Binding var selectedLeague: League // if remove #State the data will pass fine
var body: some View {
Menu {
ForEach(vm.leagues) { league in
Button {
self.selectedLeague = league
} label: {
Text(league.name)
}
}
} label: {
Image(systemName: "calendar")
}
}
}
Main goals:
Show selected league seasons data in DetailView()
Possibility to change seasons data in DetailView() when another league was chosen in ChangeButton()
You update view model but DetailView contains a copy of league (because it is value type).
The simplest seems to me is to return in callback seasons, so there is possibility to update local league as well.
func loadSeasons(forLeague id: String, completion: (([Season]) -> Void)?) async throws {
// ...
await MainActor.run {
if let responsedLeagueIndex = leagues.firstIndex(where: { $0.id == id }),
let unwrappedSeasons = seasons.data?.seasons {
leagues[responsedLeagueIndex].seasons = unwrappedSeasons
completion?(unwrappedSeasons) // << here !!
}
}
}
and make task dependent on league id so selection would work, like:
struct DetailView: View {
#EnvironmentObject var vm: LeagueViewModel
#State var league: League
var body: some View {
VStack {
// ...
}
.task(id: league.id) { // << here !!
Task {
try await vm.loadSeasons(forLeague: league.id) {
league.seasons = $0 // << update local copy !!
}
}
}
Tested with Xcode 13.4 / iOS 15.5
Test module is here
One question is if you already made LeagueViewModel an ObservableObject, why don't you display from it directly, and simply pass an id to your DetailView?
So your detail view will be:
struct DetailView: View {
#EnvironmentObject var vm: LeagueViewModel
#State var id: String
var body: some View {
VStack {
if let unwrappedSeasons = vm.leagues.first { $0.id == id }?.seasons {
List {
ForEach(unwrappedSeasons, id: \.year) { season in
Text(season.displayName)
}
}
} else {
ProgressView()
}
}
.task(id: id) {
Task {
try await vm.loadSeasons(forLeague: id)
}
}
.navigationBarTitle(Text("League Detail"), displayMode: .inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
ChangeButton(selectedId: $id)
}
}
}
}
The view will automatically update season data as it loads them.

How we can change text of button when a button is clicked for list items in SwiftUI?

I have list view in SwiftUI, and when I use context menu, I want to put the star icon for item as a Favorite or Unfavorite , when I click the text button, it show the Favorite text, but when I click the another item it show Unfavorite text, I do not know how to solve it, any idea?
favoritemodel:
import SwiftUI
struct FavoriteModel{
var isFavorite: Bool = false
var timeStamp: Date = Date()
var userIds: Set<UUID> = []
mutating func toogleFavorite(userId: UUID){
isFavorite.toggle()
timeStamp = Date()
if isFavorite{
userIds.insert(userId)
}else{
userIds.remove(userId)
}
}
}
model:
struct Restaurant: Identifiable {
var id = UUID()
var name: String
var image: String
var isFavorite: Bool = false
}
BasicImageRow:
struct BasicImageRow: View {
var restaurant: Restaurant
var body: some View {
HStack {
Image(restaurant.image)
.resizable()
.frame(width: 40, height: 40)
.cornerRadius(5)
Text(restaurant.name)
if restaurant.isFavorite {
Spacer()
Image(systemName: "star.fill")
.foregroundColor(.yellow)
}
}
}
}
view:
import SwiftUI
struct ContentView: View {
#State var showAnswer = false
#State var toggleText = false
#State private var selectedRestaurant: Restaurant?
#State private var restaurants = [ Restaurant(name: "Cafe Deadend", image: "cafedeadend"),
Restaurant(name: "Homei", image: "homei"),
Restaurant(name: "Teakha", image: "teakha"),
Restaurant(name: "Cafe Loisl", image: "cafeloisl"),
Restaurant(name: "Petite Oyster", image: "petiteoyster"),
Restaurant(name: "For Kee Restaurant", image: "forkeerestaurant"),
]
var body: some View {
List {
ForEach(restaurants) { restaurant in
BasicImageRow(restaurant: restaurant)
.contextMenu {
Button(action: {
self.showAnswer = true
self.toggleText.toggle()
self.setFavorite(item: restaurant)
}) {
HStack {
Text(toggleText ? "Favorite" : "UnFavorite")
Text(toggleText ? "UnFavorite" : "Favorite")
Image(systemName: "star")
}
}
}
.onTapGesture {
self.selectedRestaurant = restaurant
}
.actionSheet(item: self.$selectedRestaurant) { restaurant in
ActionSheet(title: Text("What do you want to do"), message: nil, buttons: [
.default(Text("Mark as Favorite"), action: {
self.setFavorite(item: restaurant)
}),
.cancel()
])
}
}
}
}
private func setFavorite(item restaurant: Restaurant) {
if let index = self.restaurants.firstIndex(where: { $0.id == restaurant.id }) {
self.restaurants[index].isFavorite.toggle()
}
}
}
If I understand the question correctly, try this approach by removing all toggleText and using #Ali Momeni advice:
struct ContentView: View {
#State var showAnswer = false
#State private var selectedRestaurant: Restaurant?
#State private var restaurants = [ Restaurant(name: "Cafe Deadend", image: "cafedeadend"),
Restaurant(name: "Homei", image: "homei"),
Restaurant(name: "Teakha", image: "teakha"),
Restaurant(name: "Cafe Loisl", image: "cafeloisl"),
Restaurant(name: "Petite Oyster", image: "petiteoyster"),
Restaurant(name: "For Kee Restaurant", image: "forkeerestaurant")
]
private func RestaurantRow(restaurant: Restaurant) -> some View {
BasicImageRow(restaurant: restaurant)
.contextMenu {
Button(action: {
showAnswer = true
setFavorite(item: restaurant)
}) {
HStack {
Text(restaurant.isFavorite ? "UnFavorite" : "Favorite")
Image(systemName: "star")
}
}
}.id(restaurant.id)
.onTapGesture {
selectedRestaurant = restaurant
}
.actionSheet(item: $selectedRestaurant) { restaurant in
ActionSheet(title: Text("What do you want to do"),
message: nil,
buttons: [
.default(
Text(restaurant.isFavorite ? "Mark as UnFavorite" : "Mark as Favorite"),
action: {
setFavorite(item: restaurant)
}),
.cancel()
])
}
}
var body: some View {
List {
ForEach(restaurants) { restaurant in
if restaurant.isFavorite {
RestaurantRow(restaurant: restaurant)
} else {
RestaurantRow(restaurant: restaurant)
}
}
}
}
private func setFavorite(item restaurant: Restaurant) {
if let index = restaurants.firstIndex(where: { $0.id == restaurant.id }) {
restaurants[index].isFavorite.toggle()
}
}
}
I recommend changing your model to be like how Apple's Fruta sample implements favourites. E.g.
class Model: ObservableObject {
...
#Published var favoriteSmoothieIDs = Set<Smoothie.ID>()
func toggleFavorite(smoothieID: Smoothie.ID) {
if favoriteSmoothieIDs.contains(smoothieID) {
favoriteSmoothieIDs.remove(smoothieID)
} else {
favoriteSmoothieIDs.insert(smoothieID)
}
}
func isFavorite(smoothie: Smoothie) -> Bool {
favoriteSmoothieIDs.contains(smoothie.id)
}
...
}
struct SmoothieFavoriteButton: View {
#EnvironmentObject private var model: Model
var isFavorite: Bool {
guard let smoothieID = model.selectedSmoothieID else { return false }
return model.favoriteSmoothieIDs.contains(smoothieID)
}
var body: some View {
Button(action: toggleFavorite) {
if isFavorite {
Label {
Text("Remove from Favorites", comment: "Toolbar button/menu item to remove a smoothie from favorites")
} icon: {
Image(systemName: "heart.fill")
}
} else {
Label {
Text("Add to Favorites", comment: "Toolbar button/menu item to add a smoothie to favorites")
} icon: {
Image(systemName: "heart")
}
}
}
.disabled(model.selectedSmoothieID == nil)
}
func toggleFavorite() {
guard let smoothieID = model.selectedSmoothieID else { return }
model.toggleFavorite(smoothieID: smoothieID)
}
}
struct SmoothieFavoriteButton_Previews: PreviewProvider {
static var previews: some View {
SmoothieFavoriteButton()
.padding()
.previewLayout(.sizeThatFits)
.environmentObject(Model())
}
}
As you can see using structs for model data does require some re-thinking.

Initializers with different stored properties

I have the following view where I pass a binding to an item that I need to be selected.
struct SelectionListView<Data>: View where Data: RandomAccessCollection, Data.Element: Identifiable, Data.Element: Named {
private let data: Data
#Binding private var isPresented: Bool
#Binding private var selectedElement: Data.Element
init(
data: Data,
selectedElement: Binding<Data.Element>,
isPresented: Binding<Bool>
) {
self.data = data
_selectedElement = selectedElement
_isPresented = isPresented
}
var body: some View {
VStack {
ForEach(data) { element in
Button(element.name) {
selectedElement = element
isPresented.toggle()
}
.foregroundColor(
selectedElement.id == item.id
? .black
: .white
)
}
}
}
}
I would need a slightly different initializer of this view where I can only pass the element ID, instead of the whole element. I'm having trouble achieving this solution. To make it even more clear, it would be great if I could have a second initializer such that:
init(
data: Data,
selectedId: Binding<Data.Element.ID>,
isPresented: Binding<Bool>
)
Here is a working version. I decided to store the element or id in their own enum cases. I made the view separate just so it is a little easier to understand what I did.
Working code:
struct SelectionListView<Data>: View where Data: RandomAccessCollection, Data.Element: Identifiable, Data.Element: Named {
enum Selected {
case element(Binding<Data.Element>)
case id(Binding<Data.Element.ID>)
}
#Binding private var isPresented: Bool
private let data: Data
private let selected: Selected
init(
data: Data,
selectedElement: Binding<Data.Element>,
isPresented: Binding<Bool>
) {
self.data = data
selected = .element(selectedElement)
_isPresented = isPresented
}
init(
data: Data,
selectedId: Binding<Data.Element.ID>,
isPresented: Binding<Bool>
) {
self.data = data
selected = .id(selectedId)
_isPresented = isPresented
}
var body: some View {
SelectionListItem(data: data) { dataElement in
switch selected {
case .element(let element):
element.wrappedValue = dataElement
print("Selected element:", element.wrappedValue)
case .id(let id):
id.wrappedValue = dataElement.id
print("Selected element ID:", id.wrappedValue)
}
isPresented.toggle()
}
}
}
struct SelectionListItem<Data>: View where Data: RandomAccessCollection, Data.Element: Identifiable, Data.Element: Named {
let data: Data
let action: (Data.Element) -> Void
var body: some View {
VStack {
ForEach(data) { element in
Button(element.name) {
action(element)
}
.foregroundColor(
.red // Temporary because I don't know what `item.id` is
// selectedElement.id == item.id
// ? .black
// : .white
)
}
}
}
}
Other code for minimal working example:
struct ContentView: View {
#State private var selection: StrItem
#State private var selectionId: StrItem.ID
#State private var isPresented = true
private let data: [StrItem]
init() {
data = [StrItem("Hello"), StrItem("world!")]
_selection = State(initialValue: data.first!)
_selectionId = State(initialValue: data.first!.id)
}
var body: some View {
// Comment these to try each initializer
//SelectionListView(data: data, selectedElement: $selection, isPresented: $isPresented)
SelectionListView(data: data, selectedId: $selectionId, isPresented: $isPresented)
}
}
protocol Named {
var name: String { get }
}
struct StrItem: Identifiable, Named {
let id = UUID()
let str: String
var name: String { id.uuidString }
init(_ str: String) {
self.str = str
}
}
I'm not really sure what you are trying to achieve. Something feels off :) But anyway, here's a variant of your code that would do what you want:
struct SelectionListView<Data>: View where Data: RandomAccessCollection, Data.Element: Identifiable, Data.Element: Named {
private let data: Data
#Binding private var isPresented: Bool
#Binding private var selectedElement: Data.Element
#Binding private var selectedId: Data.Element.ID
init(
data: Data,
selectedElement: Binding<Data.Element>,
isPresented: Binding<Bool>
) {
self.data = data
_selectedElement = selectedElement
_selectedId = .constant(selectedElement.wrappedValue.id)
_isPresented = isPresented
}
init(
data: Data,
selectedId: Binding<Data.Element.ID>,
isPresented: Binding<Bool>
) {
self.data = data
_selectedElement = .constant(data.first(where: { $0.id == selectedId.wrappedValue })!)
_selectedId = selectedId
_isPresented = isPresented
}
var body: some View {
VStack {
ForEach(data) { element in
Button(element.name) {
selectedElement = element
selectedId = element.id
isPresented.toggle()
}
.foregroundColor(
selectedElement.id == element.id
? .black
: .gray
)
}
}
}
}

In SwiftUI view is not updating as the model gets updated

In the project, I call Google's eBook API to list books. It lists Harry Potter books by default. Beside this I have a search bar to search for book of other topic like 'java'. And it works fine, when I start writing in the search field my View Model updates the array of books in Model. However, it doesn't update my view at all. I have provided all the codes below.
Model:
import Foundation
struct BookModel {
var books: [Book] = []
struct Book: Identifiable {
var id: String
var title: String
var authors: String
var desc: String
var imurl: String
var url: String
}
}
ViewModel:
import Foundation
import SwiftyJSON
class BookViewModel: ObservableObject {
#Published var model = BookModel()
init(searchText: String) {
let url = "https://www.googleapis.com/books/v1/volumes?q=\(searchText)"
print(url)
let session = URLSession(configuration: .default)
session.dataTask(with: URL(string: url)!) { (resp, _, error) in
if error != nil {
print(error?.localizedDescription ?? "Error")
return
}
let json = try! JSON(data: resp!)
let items = json["items"].array!
for item in items {
let id = item["id"].stringValue
let title = item["volumeInfo"]["title"].stringValue
let authors = item["volumeInfo"]["authors"].array ?? []
var author: String = ""
for name in authors {
author += "\(name.stringValue)"
}
let description = item["volumeInfo"]["description"].stringValue
let imurl = item["volumeInfo"]["imageLinks"]["thumbnail"].stringValue
let webReaderLink = item["volumeInfo"]["previewLink"].stringValue
print(title)
DispatchQueue.main.async {
self.model.books.append(BookModel.Book(id: id, title: title, authors: author, desc: description, imurl: imurl, url: webReaderLink))
}
}
}
.resume()
// For testing
for i in model.books {
print(i.title)
}
}
//MARK:- Access to the model
var books: [BookModel.Book] {
model.books
}
}
ContentView:
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel = BookViewModel(searchText: "harry+potter")
var body: some View {
CustomNavigationView(view: AnyView(Home(viewModel: viewModel)), placeHolder: "Search", largeTitle: true, title: "Books") { (text) in
if text != "" {
BookViewModel(searchText: text.lowercased().replacingOccurrences(of: " ", with: "+"))
}
} onCancel: {
BookViewModel(searchText: "harry+potter")
}
.edgesIgnoringSafeArea(.all)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Home:
import SwiftUI
import SDWebImageSwiftUI
struct Home: View {
#ObservedObject var viewModel: BookViewModel
#State private var show: Bool = false
#State var previewURL = ""
var body: some View {
List {
ForEach(viewModel.books) { book in
HStack {
if book.imurl != "" {
WebImage(url: URL(string: book.imurl))
.resizable()
.frame(width: 120, height: 170)
} else {
Image(systemName: "character.book.closed.fill")
.font(.system(size: 60))
.frame(width: 120, height: 170)
}
VStack(alignment: .leading, spacing: 10) {
Text(book.title)
.fontWeight(.bold)
Text(book.authors)
Text(book.desc)
.font(.system(size: 13))
.lineLimit(4)
.multilineTextAlignment(.leading)
}
}
.onTapGesture {
self.previewURL = book.url
show.toggle()
}
}
}
.sheet(isPresented: $show) {
NavigationView {
WebView(url: $previewURL)
.navigationBarTitle("Book Preview")
}
}
}
}
CustomNavigationView:
import SwiftUI
struct CustomNavigationView: UIViewControllerRepresentable {
func makeCoordinator() -> Coordinator {
return CustomNavigationView.Coordinator(parent: self)
}
var view: AnyView
//onSearch and onCancel Closures
var onSearch: (String) -> ()
var onCancel: () -> ()
var title: String
var largeTitle: Bool
var placeHolder: String
init(view: AnyView, placeHolder: String? = "Search", largeTitle: Bool? = false, title: String, onSearch: #escaping (String) -> (), onCancel: #escaping () -> ()) {
self.title = title
self.largeTitle = largeTitle!
self.placeHolder = placeHolder!
self.view = view
self.onSearch = onSearch
self.onCancel = onCancel
}
func makeUIViewController(context: Context) -> UINavigationController {
let childView = UIHostingController(rootView: view)
let controller = UINavigationController(rootViewController: childView)
controller.navigationBar.topItem?.title = title
controller.navigationBar.prefersLargeTitles = largeTitle
let searchController = UISearchController()
searchController.searchBar.placeholder = placeHolder
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.delegate = context.coordinator
controller.navigationBar.topItem?.hidesSearchBarWhenScrolling = false
controller.navigationBar.topItem?.searchController = searchController
return controller
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {
uiViewController.navigationBar.topItem?.title = title
uiViewController.navigationBar.topItem?.searchController?.searchBar.placeholder = placeHolder
uiViewController.navigationBar.prefersLargeTitles = largeTitle
}
//Search Bar Delegate
class Coordinator: NSObject, UISearchBarDelegate {
var parent: CustomNavigationView
init(parent: CustomNavigationView) {
self.parent = parent
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
self.parent.onSearch(searchText)
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
self.parent.onCancel()
}
}
}
Full Project link:
https://github.com/shawkathSrijon/eBook-Reader.git
You might have to tell your views about new changes using the ObservableObjectPublisher objectWillChange.
DispatchQueue.main.async {
self.model.books.append(BookModel.Book(id: id, title: title, authors: author, desc: description, imurl: imurl, url: webReaderLink))
self.objectWillChange.send() // <-- Here
}