Access REST API in SwiftUI preview - swiftui

I have a simple view with list of data:
struct ContentView: View {
#ObservedObject var model = AdListViewModel()
var body: some View {
List(model.adList, id: \._id) { ad in
Text(String(describing: ad._id ?? -1))
}
}
}
where model fetches data from REST API:
final class AdListViewModel: ObservableObject {
#Published var adList = [Ad]()
init() {
DataAccessAPI.getAllAd(page: 1) { ads, error in
if (error == nil) {
if let newAds = ads {
self.adList.append(contentsOf: newAds)
}
}
}
}
}
Is it possible to fetch data in preview mode?

Related

Using ForEach inside a Picker

I'm having issues pulling data from an Array into a picker using SwiftUI. I can correctly make a list of the data I'm interested in, but can't seem to make the same logic work to pull the data into a picker. I've coded it a few different ways but the current way I have gives this error:
Referencing initializer 'init(_:content:)' on 'ForEach' requires that 'Text' conform to 'TableRowContent'
The code is below:
import SwiftUI
struct BumpSelector: View {
#ObservedObject var model = ViewModel()
#State var selectedStyle = 0
init(){
model.getData2()}
var body: some View {
VStack{
List (model.list) { item in
Text(item.style)}
Picker("Style", selection: $selectedStyle, content: {
ForEach(0..<model.list.count, content: { index in
Text(index.style)
})
})
}
}
The model is here:
import Foundation
struct Bumps: Identifiable{
var id: String
var style: String
}
and the ViewModel is here:
import Foundation
import Firebase
import FirebaseFirestore
class ViewModel: ObservableObject {
#Published var list = [Bumps]()
#Published var styleArray = [String]()
func getData2() {
let db = Firestore.firestore()
db.collection("bumpStop").getDocuments { bumpSnapshot, error in
//Check for errors first:
if error == nil {
//Below ensures bumpSnapshot isn't nil
if let bumpSnapshot = bumpSnapshot {
DispatchQueue.main.async {
self.list = bumpSnapshot.documents.map{ bump in
return Bumps(id: bump.documentID,
style: bump["style"] as? String ?? "")
}
}
}
}
else {
//Take care of the error
}
}
}
}
index in your ForEach is just an Int, there is no style associated with an Int. You could try this approach to make the Picker work with its ForEach:
struct BumpSelector: View {
#ObservedObject var model = ViewModel()
#State var selectedStyle = 0
init(){
model.getData2()
}
var body: some View {
VStack{
List (model.list) { item in
Text(item.style)}
Picker("Style", selection: $selectedStyle) {
ForEach(model.list.indices, id: \.self) { index in
Text(model.list[index].style).tag(index)
}
}
}
}
}
EDIT-1:
Text(model.list[selectedStyle].style) will give you the required style of the selectedStyle.
However, as always when using index, you need to ensure it is valid at the time of use.
That is, use if selectedStyle < model.list.count { Text(model.list[selectedStyle].style) }.
You could also use this alternative approach that does not use index:
struct Bumps: Identifiable, Hashable { // <-- here
var id: String
var style: String
}
struct BumpSelector: View {
#ObservedObject var model = ViewModel()
#State var selectedBumps = Bumps(id: "", style: "") // <-- here
init(){
model.getData2()
}
var body: some View {
VStack{
List (model.list) { item in
Text(item.style)
}
Picker("Style", selection: $selectedBumps) {
ForEach(model.list) { bumps in
Text(bumps.style).tag(bumps) // <-- here
}
}
}
.onAppear {
if let first = model.list.first {
selectedBumps = first
}
}
}
}
Then use selectedBumps, just like any Bumps, such as selectedBumps.style

How to keep a Bool state after Google AdMob RewardAd has finished displaying?

I'm currently developing an application using SwiftUI.
I'm trying to use a google AdMob reward Ad.
I made codes to show reward Ads referring to this article.
I'm trying to show an alert after I finish watching a reward Ad fully using a Bool state from RewardedAdDelegate class but It doesn't work...
How could I solve this problem?
Here are the codes:
AdShow.swift
import SwiftUI
struct AdShow: View {
#ObservedObject var adDelegate = RewardedAdDelegate()
var body: some View {
RewardedAd()
.alert(isPresented: $adDelegate.adFullyWatched){
Alert(title: Text("reward Ad finished"),
message: Text("reward Ad finished"),
dismissButton: .default(Text("OK")))
}
}
}
RewardedAd.swift
import SwiftUI
import GoogleMobileAds
struct RewardedAd: View {
#EnvironmentObject var appState: AppState
#ObservedObject var adDelegate = RewardedAdDelegate()
var body: some View {
if adDelegate.adLoaded && !adDelegate.adFullyWatched {
let root = UIApplication.shared.windows.first?.rootViewController
self.adDelegate.rewardedAd!.present(fromRootViewController: root!, delegate: adDelegate)
}
return Text("Load ad").onTapGesture {
self.adDelegate.loadAd()
}
}
}
RewardedAdDelegate
import Foundation
import GoogleMobileAds
class RewardedAdDelegate: NSObject, GADRewardedAdDelegate, ObservableObject {
#Published var adLoaded: Bool = false
#Published var adFullyWatched: Bool = false
var rewardedAd: GADRewardedAd? = nil
func loadAd() {
rewardedAd = GADRewardedAd(adUnitID: "ca-app-pub-3940256099942544/1712485313")
rewardedAd!.load(GADRequest()) { error in
if error != nil {
self.adLoaded = false
} else {
self.adLoaded = true
}
}
}
/// Tells the delegate that the user earned a reward.
func rewardedAd(_ rewardedAd: GADRewardedAd, userDidEarn reward: GADAdReward) {
adFullyWatched = true
print("Reward received with currency: \(reward.type), amount \(reward.amount).")
}
/// Tells the delegate that the rewarded ad was presented.
func rewardedAdDidPresent(_ rewardedAd: GADRewardedAd) {
self.adLoaded = false
print("Rewarded ad presented.")
}
/// Tells the delegate that the rewarded ad was dismissed.
func rewardedAdDidDismiss(_ rewardedAd: GADRewardedAd) {
print("Rewarded ad dismissed.")
}
/// Tells the delegate that the rewarded ad failed to present.
func rewardedAd(_ rewardedAd: GADRewardedAd, didFailToPresentWithError error: Error) {
print("Rewarded ad failed to present.")
}
}
UPDATED
AdShow.swift
import SwiftUI
struct AdShow: View {
#ObservedObject var adDelegate = RewardedAdDelegate()
var body: some View {
VStack{
RewardedAd(adDelegate: self.adDelegate)
.alert(isPresented: $adDelegate.adFullyWatched){
Alert(title: Text(""),
message: Text(""),
dismissButton: .default(Text("OK")))
}
// check adDelegate.adFullyWatched state -> after Ad finish this text shows
if adDelegate.adFullyWatched{
Text("Checked")
}
}
}
}
RewardedAd.swift
import SwiftUI
import GoogleMobileAds
struct RewardedAd: View {
#ObservedObject var adDelegate : RewardedAdDelegate
var body: some View {
if adDelegate.adLoaded && !adDelegate.adFullyWatched {
let root = UIApplication.shared.windows.first?.rootViewController
self.adDelegate.rewardedAd!.present(fromRootViewController: root!, delegate: adDelegate)
}
return Text("Load ad").onTapGesture {
self.adDelegate.loadAd()
}
}
}
ReUPDATED
AdShow.swift
import SwiftUI
struct AdShow: View {
#ObservedObject var adDelegate = RewardedAdDelegate()
#State var isAlert = false
var body: some View {
VStack{
Button(action: {
self.isAlert = true
}){
Text("alert")
.padding()
}
RewardedAd(adDelegate: self.adDelegate)
.alert(isPresented: self.$isAlert){
Alert(title: Text(""),
message: Text(""),
dismissButton: .default(Text("OK")))
}
// check adDelegate.adFullyWatched state -> after Ad finish this text shows
if adDelegate.adFullyWatched{
Text("Checked")
}
}
.onAppear(){
if adDelegate.adFullyWatched{
self.isAlert = true
}
}
}
}
Xcode: Version 12.0.1
iOS: 13.0
You use different instances of delegate in your views, instead you have to inject delegate from first view into second one.
struct AdShow: View {
#ObservedObject var adDelegate = RewardedAdDelegate()
var body: some View {
RewardedAd(adDelegate: self.adDelegate) // << here inject !!
// ... other code
and
struct RewardedAd: View {
#EnvironmentObject var appState: AppState
#ObservedObject var adDelegate: RewardedAdDelegate // << here declare !!
// ... other code

How to refresh detail view and master view at the same time in ios14.2 (SwiftUI)

Hi I want to refresh the detail view which is triggered by data change under the master view
My approach is setup a Combine listener and trigger the refresh from the detail view
It was working on ios13 but I can't make it working on ios14.2, any workaround?
Here is my master view.
import SwiftUI
struct MainView: View {
#ObservedObject var viewModel : MainViewModel
init(){
viewModel = MainViewModel.shared
}
var body: some View {
NavigationView{
List {
ForEach(viewModel.games ,id:\.self) { game in
NavigationLink(destination: self.navDest(game: game)){
Text("\(game)")
}
}
}
}
}
func navDest(game: Int) -> some View{
print("games value:",game)
return LazyView(DetailView(game: game ))
}
}
The master view model listen to the event come from detail view and update the value
import Foundation
import SwiftUI
import Combine
class MainViewModel : ObservableObject {
#Published var games : [Int] = []
static let shared = MainViewModel()
private var tickets: [AnyCancellable] = []
init(){
for i in 0...5{
games.append(i)
}
addObservor()
}
func addObservor(){
NotificationCenter.default.publisher(for: .updateGame)
.map{$0.object as! Int}
.sink { [unowned self] (game) in
self.updateGame(game: game)
}.store(in: &tickets)
}
func updateGame(game:Int){
print("updateView index:",game)
self.games[game] = 9999
print("after update",games)
}
}
import SwiftUI
struct DetailView: View {
#ObservedObject var viewModel : DetailViewModel
init(game: Int) {
print("init detail",game)
viewModel = DetailViewModel(game:game)
}
var body: some View {
VStack{
Text("\(viewModel.game)")
Button("update"){
viewModel.sendUpdate()
}
}
}
}
When I click the update from detail view, it should refresh the master and detail view at the same time.(The flow is DetailViewModlel->MainViewModlel->Refresh MainView ->Refresh DetailView (which is currently display)) But it not work on iOS 14.2
import Foundation
import SwiftUI
class DetailViewModel : ObservableObject {
var period : String = ""
var game : Int = 0
// init(period:String,game: Int){
// self.period = period
init(game: Int){
self.game = game
}
func sendUpdate(){
NotificationCenter.default.post(name: .updateGame, object: game)
}
}
extension Notification.Name {
static let updateGame = Notification.Name("updateGame")
}
struct LazyView<Content: View>: View {
let build: () -> Content
init(_ build: #autoclosure #escaping () -> Content) {
self.build = build
}
var body: Content {
build()
}
}
DetailView
import SwiftUI
struct DetailView: View {
#ObservedObject var viewModel : DetailViewModel
init(game: Int) {
print("init detail",game)
viewModel = DetailViewModel(game:game)
}
var body: some View {
VStack{
Text("\(viewModel.game)")
Button("update"){
viewModel.sendUpdate()
}
}
}
}
I have logged down the debug message, it seem like the data did changed but the view didn't.

SwiftUI can not update class data updates

I came across a situation that you use class data as your data source, and display them in a swiftUI list view, when you update your data source, the swiftUI list view won't be updated, what can we do to make the class data updates interactive with swiftUI?
see code blow:
I define the environment object :
import Foundation
import Combine
class DataSource: ObservableObject {
public static let shared = DataSource()
#Published var datalist: [RowData] = []
func fetch() -> Void {
for n in 1...50 {
let data = RowData(title: "Index:\(n)", count: 0)
datalist.insert(data, at: 0)
}
}
func update() {
for data in datalist {
data.count = data.count+1
print("\(data.title) update count to :\(data.count)")
data.objectWillChange.send()
}
self.objectWillChange.send()
}
}
to display each data in a Row View:
import SwiftUI
struct RowView: View {
#State var data: RowData
var body: some View {
HStack{
Text(data.title)
Spacer()
Text("\(data.count)")
}.padding()
}
}
struct RowView_Previews: PreviewProvider {
static var previews: some View {
RowView(data: RowData(title: "text", count: 1))
}
}
class RowData: ObservableObject {
var title: String = ""
var count: Int = 0
init(title: String, count: Int) {
self.title = title
self.count = count
}
}
in content view, display the data in a list view, I would like to refresh all the view updates when click update button. the button triggers the update methods to update the class data value from data source.
struct ContentView: View {
#EnvironmentObject var data: DataSource
#State var shouldUpdate:Bool = false
#State var localData:[RowData] = []
var body: some View {
VStack {
Button(action: {
// your action here
self.data.update()
self.shouldUpdate.toggle()
self.localData.removeAll()
self.localData = self.data.datalist
}) {
Text("update")
}
List {
ForEach(0..<self.localData.count, id:\.self) { index in
RowView(data: self.localData[index])
}
}
}
}
}
Well... I don't see the reason to have localData, but, anyway, here is modified code that works.
Tested with Xcode 12 / iOS 14
class DataSource: ObservableObject {
public static let shared = DataSource()
#Published var datalist: [RowData] = []
func fetch() -> Void {
for n in 1...50 {
let data = RowData(title: "Index:\(n)", count: 0)
datalist.insert(data, at: 0)
}
}
func update() {
for data in datalist {
data.count = data.count+1
print("\(data.title) update count to :\(data.count)")
}
self.objectWillChange.send()
}
}
struct RowView: View {
#ObservedObject var data: RowData
var body: some View {
HStack{
Text(data.title)
Spacer()
Text("\(data.count)")
}.padding()
}
}
class RowData: ObservableObject {
#Published var title: String = ""
#Published var count: Int = 0
init(title: String, count: Int) {
self.title = title
self.count = count
}
}
struct ContentView: View {
#EnvironmentObject var data: DataSource
#State var localData:[RowData] = []
var body: some View {
VStack {
Button(action: {
// your action here
self.data.update()
self.localData = self.data.datalist
}) {
Text("update")
}
List {
ForEach(0..<self.localData.count, id:\.self) { index in
RowView(data: self.localData[index])
}
}
}
.onAppear {
self.data.fetch()
self.localData = self.data.datalist
}
}
}

Why .append doesn't reload swiftUI view

I have the following view hierarchy
Nurse List View > Nurse Card > Favorite button
Nurse List View
struct NurseListView: View {
#State var data: [Nurse] = []
var body: some View {
List {
ForEach(data.indices, id: \.self) { index in
NurseCard(data: self.$data[index])
}
}
}
}
Nurse Card
struct NurseCard: View {
#Binding var data: Nurse
var body: some View {
FavoriteActionView(data:
Binding(
get: { self.data },
set: { self.data = $0 as! Nurse }
)
)
}
}
Favorite Action View
struct FavoriteActionView: View {
#Binding var data: FavoritableData
var body: some View {
Button(action: {
self.toggleFavIcon()
}) {
VStack {
Image(data.isFavorite ? "fav-icon" : "not-fav-icon")
Text(String(data.likes.count))
}
}
}
private func toggleFavIcon() {
if data.isFavorite {
if let index = data.likes.firstIndex(of: AppState.currentUser.uid) {
data.likes.remove(at: index)
}
} else {
data.likes.append(AppState.currentUser.uid)
}
}
}
When toggleFavIcon execute, it append/remove the user id from the likes property in data object but I can't see the change unless I go back to previous page and reopen the page. What I am missing here?
As Asperi wrote, using an ObservableObject would work well here. Something like this:
class FavoritableData: ObservableObject {
#Published var likes: [String] = []
#Published var isFavorite = false
}
struct FavoriteActionView: View {
#ObservedObject var data: FavoritableData
var body: some View {
Button(action: {
self.toggleFavIcon()
}) {
VStack {
Image(data.isFavorite ? "fav-icon" : "not-fav-icon")
Text(String(data.likes.count))
}
}
}
private func toggleFavIcon() {
if data.isFavorite {
if let index = data.likes.firstIndex(of: AppState.currentUser.uid) {
data.likes.remove(at: index)
}
} else {
data.likes.append(AppState.currentUser.uid)
}
}
}