Compatibility of init Binding (in WebView) and State (in View) - ios

I'm implementing a sheet between a web view and a view. The values ​​in the sheet are always changing. The value of the sheet called by the view must be shared by the web view (or reverse). There is no problem when mounting the web view directly inside the view. The problem arises when mediating with 'let' expressions.
struct ItlnrView: View {
#State var activeSheet: **Bool** = false
.....
let webView = myWebView(web: nil, actSheet: **$activeSheet**, req: URLRequest(url: URL(string: "https://google.com")!)). <--- problem
var body: some View {
VStack {
webView
// myWebView(web: nil, actSheet: $activeSheet, req: URLRequest(url: URL(string: "https://google.com")!)) <--- no problem
}
.sheet(isPresented: $activeSheet) {
// code
}
}
}
struct myWebView: UIViewRepresentable {
let request: URLRequest
var webview: WKWebView?
#Binding var activeSheet: Bool
init(web: WKWebView?, actSheet: **Binding<Bool>**, req: URLRequest) {
self.webview = WKWebView()
self._activeSheet = actSheet
self.request = req
}
....
....
}
If I don't use let webView and run myWebView(web:,actSheet:,req:) in V/HStack directly, the sheet value is compatible/shared properly. But I must use/call by let webView because I have to use the 'go back' and 'reload' functions of web view. So whenever I use let webView, #State and #Binding then are incompatible.
In other words, $activeSheet requires as a Binding value in let webView, but #State value is appropriate when coding myWebView(web:,actSheet:,req:) in Stack directly. I hope you to be understand my lacked question.

It seems to be a bug that the same #State type and #Binding type are not compatible in the let webview environment.
I solved myself the problem that #State and #Binding are not compatible when calling let webView. As follows:
struct ItlnrView: View {
#State var activeSheet: Bool = false
#State var urlToOpenInSheet: String = "" <--- add
.....
//let webView = myWebView(web: nil, actSheet: **$activeSheet**, req: URLRequest(url: URL(string: "https://google.com")!)) <--- not required
var body: some View {
VStack {
// webView <--- not required
myWebView(web: nil, actSheet: $activeSheet, **urlToSheet: $urlToOpenInSheet**, req: URLRequest(url: URL(string: "https://google.com")!))
}
.sheet(isPresented: $activeSheet) {
// code
}
}
}
struct myWebView: UIViewRepresentable {
let request: URLRequest
var webview: WKWebView?
#Binding var activeSheet: Bool
#Binding var urlToOpenInSheet: String <---- add
init(web: WKWebView?, actSheet: Binding<Bool>, **urlToSheet: Binding<String>**, req: URLRequest) {
self.webview = WKWebView()
self._activeSheet = actSheet
elf._urlToOpenInSheet = urlToSheet <---- add
self.request = req
}
....
....
func updateUIView(_ uiView: WKWebView, context: Context) {
// add follow
// 'uiView.load(request)' will replace 'let webView'(that was in View)
if urlToOpenInSheet == "" {
DispatchQueue.main.async {
uiView.load(request)
}
}
}
}
As a result, urlToOpenInSheet brings two webview environments.

Related

How to navigate between two API parameters in SwiftUI

I'm beginner of iOS app development, currently doing iOS & Swift Bootcamp on Udemy by Angela Yu. I have this app called H4X0R News, which shows Hacker News all stories that are on the front/home page on the app by using its API. By the end of a module the app works fine when url property from API json is not nil but there are certain cases when url equals nil. These are posts which instead has story_text property. So what I want here to adjust is add story_text to my code and use it to navigate between this and url parameter. Here's the code I've got:
ContentView.swift
import SwiftUI
struct ContentView: View {
#ObservedObject var networkManager = NetworkManager()
var body: some View {
NavigationView {
List(networkManager.posts) { post in
NavigationLink(destination: DetailView(url: post.url)) {
HStack {
Text(String(post.points))
Text(post.title)
}
}
}
.navigationTitle("H4X0R NEWS")
}
.onAppear {
self.networkManager.fechData()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
WebView.swift
import Foundation
import WebKit
import SwiftUI
struct WebView: UIViewRepresentable {
let urlString: String?
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
if let safeString = urlString {
if let url = URL(string: safeString) {
let request = URLRequest(url: url)
uiView.load(request)
}
}
}
}
DetailView.swift
import SwiftUI
struct DetailView: View {
let url: String?
var body: some View {
WebView(urlString: url)
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(url: "https://www.google.com/")
}
}
NetworkManager.swift
import Foundation
class NetworkManager: ObservableObject {
#Published var posts = [Post]()
func fechData() {
if let url = URL(string: "http://hn.algolia.com/api/v1/search?tags=front_page") {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { data, response, error in
if error == nil {
let decoder = JSONDecoder()
if let safeData = data {
do {
let results = try decoder.decode(Results.self, from: safeData)
DispatchQueue.main.async {
self.posts = results.hits
}
} catch {
print(error)
}
}
}
}
task.resume()
}
}
}
PostData.swift
import Foundation
struct Results: Decodable {
let hits: [Post]
}
struct Post: Decodable, Identifiable {
var id: String {
return objectID
}
let objectID: String
let points: Int
let title: String
let url: String?
}
So what I'm sure I need to add this story_text to PostData as String? and then make conditional statement in the WebView.updateUIView() function. Then update the code in other files. But like I said, I'm new in the programming world and I seek for help here for the first time since I've started the course.
If I understand you correct, you want to navigate to a simple text view if there is no URL for the Post?
So you can do it like this:
let text: String?
var body: some View {
if let url = url {
WebView(urlString: url)
} else {
Text(text ?? "-")
}
}

Where to make an API call to populate a view with data

I am trying to create a view that displays results from an API call, however I keep on running into multiple errors.
My question is basically where is the best place to make such an API call.
Right now I am "trying" to load the data in the "init" method of the view like below.
struct LandingView: View {
#StateObject var viewRouter: ViewRouter
#State var user1: User
#State var products: [Product] = []
init(_ viewRouter : ViewRouter, user: User) {
self.user1 = user
_viewRouter = StateObject(wrappedValue: viewRouter)
ProductAPI().getAllProducts { productArr in
self.products = productArr
}
}
var body: some View {
tabViewUnique(prodArrParam: products)
}
}
I keep on getting an "escaping closure mutating self" error, and while I could reconfigure the code to stop the error,I am sure that there is a better way of doing what I want.
Thanks
struct ContentView: View {
#State var results = [TaskEntry]()
var body: some View {
List(results, id: \.id) { item in
VStack(alignment: .leading) {
Text(item.title)
}
// this one onAppear you can use it
}.onAppear(perform: loadData)
}
func loadData() {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/todos") else {
print("Your API end point is Invalid")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let response = try? JSONDecoder().decode([TaskEntry].self, from: data) {
DispatchQueue.main.async {
self.results = response
}
return
}
}
}.resume()
}
}
In .onAppear you can make api calls

How to manage AVPlayer state in SwiftUI

I have a list of URLs in SwiftUI. When I tap an item, I present a full screen video player. I have an #EnvironmentObject that handles some viewer options (for example, whether to show a timecode). I also have a toggle that shows and hides the timecode (I've only included the toggle in this example as the timecode view doesn't matter) but every time I change the toggle the view is created again, which re-sets the AVPlayer. This makes sense since I'm creating the player in the view's initialiser.
I thought about creating my own ObserveredObject class to contain an AVPlayer but I'm not sure how or where I'd initialise it since I need to give it a URL, which I only know from the initialiser of CustomPlayerView. I also thought about setting the player as an #EnvironmentObject but it seems weird to initialise something I might not need (if the user doesn't tap on a URL to start the player).
What is the correct way to create an AVPlayer to hand to AVKit's VideoPlayer please? Here's my example code:
class ViewerOptions: ObservableObject {
#Published var showTimecode = false
}
struct CustomPlayerView: View {
#EnvironmentObject var viewerOptions: ViewerOptions
private let avPlayer: AVPlayer
init(url: URL) {
avPlayer = AVPlayer(url: url)
}
var body: some View {
HStack {
VideoPlayer(player: avPlayer)
Toggle(isOn: $viewerOptions.showTimecode) { Text("Show Timecode") }
}
}
}
There are a couple of approaches you can take here. You can try them out and see which one suits best for you.
Option 1: As you said you can wrap avPlayer in a new ObserveredObject class
class PlayerViewModel: ObservableObject {
#Published var avPlayer: AVPlayer? = nil
}
class ViewerOptions: ObservableObject {
#Published var showTimecode = false
}
#main
struct DemoApp: App {
var playerViewModel = PlayerViewModel()
var viewerOptions = ViewerOptions()
var body: some Scene {
WindowGroup {
CustomPlayerView(url: URL(string: "Your URL here")!)
.environmentObject(playerViewModel)
.environmentObject(viewerOptions)
}
}
}
struct CustomPlayerView: View {
#EnvironmentObject var viewerOptions: ViewerOptions
#EnvironmentObject var playerViewModel: PlayerViewModel
init(url: URL) {
if playerViewModel.avPlayer == nil {
playerViewModel.avPlayer = AVPlayer(url: url)
} else {
playerViewModel.avPlayer?.pause()
playerViewModel.avPlayer?.replaceCurrentItem(with: AVPlayerItem(url: url))
}
}
var body: some View {
HStack {
VideoPlayer(player: playerViewModel.avPlayer)
Toggle(isOn: $viewerOptions.showTimecode) { Text("Show Timecode") }
}
}
}
Option 2: You can add avPlayer to your already existing class ViewerOptions as an optional property and then initialise it when you need it
class ViewerOptions: ObservableObject {
#Published var showTimecode = false
#Published var avPlayer: AVPlayer? = nil
}
struct CustomPlayerView: View {
#EnvironmentObject var viewerOptions: ViewerOptions
init(url: URL) {
if viewerOptions.avPlayer == nil {
viewerOptions.avPlayer = AVPlayer(url: url)
} else {
viewerOptions.avPlayer?.pause()
viewerOptions.avPlayer?.replaceCurrentItem(with: AVPlayerItem(url: url))
}
}
var body: some View {
HStack {
VideoPlayer(player: viewerOptions.avPlayer)
Toggle(isOn: $viewerOptions.showTimecode) { Text("Show Timecode") }
}
}
}
Option 3: Make your avPlayer a state object this way its memory will be managed by the system and it will not re-set it and keep it alive for you until your view exists.
class ViewerOptions: ObservableObject {
#Published var showTimecode = false
}
struct CustomPlayerView: View {
#EnvironmentObject var viewerOptions: ViewerOptions
#State private var avPlayer: AVPlayer
init(url: URL) {
_avPlayer = .init(wrappedValue: AVPlayer(url: url))
}
var body: some View {
HStack {
VideoPlayer(player: avPlayer)
Toggle(isOn: $viewerOptions.showTimecode) { Text("Show Timecode") }
}
}
}
Option 4: Create your avPlayer object when you need it and forget it (Not sure this is the best approach for you but if you do not need your player object to perform custom actions then you can use this option)
class ViewerOptions: ObservableObject {
#Published var showTimecode = false
}
struct CustomPlayerView: View {
#EnvironmentObject var viewerOptions: ViewerOptions
private let url: URL
init(url: URL) {
self.url = url
}
var body: some View {
HStack {
VideoPlayer(player: AVPlayer(url: url))
Toggle(isOn: $viewerOptions.showTimecode) { Text("Show Timecode") }
}
}
}

Content view is not updating when the variable is changed

I have basic app that display a website and when I press a button it should send me to another link but it does not idk why I tried using #state bool and changing it when button is pressed but no use
the website loads but the button does not change the website
ContentView.swift
import SwiftUI
import WebKit
import Foundation
struct ContentView: View {
#State private var selectedSegment = 0
#State var websi = "172.20.10.3"
#State private var websites = ["192.168.8.125", "192.168.8.125/Receipts.php","192.168.8.125/myqr.php"]
#State private var sssa = ["Home","MyRecipts","MyQr"]
#State var updater: Bool
var body: some View {
HStack {
NavigationView{
VStack{
Button(action: {
self.websi = "172.20.10.3/myqe.php"
self.updater.toggle()
}) {
Text(/*#START_MENU_TOKEN#*/"Button"/*#END_MENU_TOKEN#*/)
} .pickerStyle(SegmentedPickerStyle());
/* Text("Selected value is: \(websites[selectedSegment])").padding()*/
Webview(url: "http://\(websi)")
}
}
.padding(.top, -44.0)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView( updater: false)
.padding(.top, -68.0)
}
}
Webview.swift
import Foundation
import SwiftUI
import WebKit
struct Webview: UIViewRepresentable {
var url: String
func makeUIView(context: Context) -> WKWebView {
guard let url = URL(string: self.url) else{
return WKWebView()
}
let request = URLRequest(url: url)
let wkWebView = WKWebView()
wkWebView.load(request)
return wkWebView
}
func updateUIView(_ uiView: WKWebView, context:
UIViewRepresentableContext<Webview>){
}
}
Because you are not recalling the WebView when the websi changes it won't do anything. You need to add a #Binding tag to the url so that it knows to update. Then you can call
WebView(self.$websi)
Here is a possible solution using an #ObservableObject:
struct WebView: UIViewRepresentable {
#ObservedObject var viewModel: ViewModel
func makeUIView(context: UIViewRepresentableContext<WebView>) -> WKWebView {
return WKWebView()
}
func updateUIView(_ webView: WKWebView, context: UIViewRepresentableContext<WebView>) {
if let url = URL(string: viewModel.url) {
webView.load(URLRequest(url: url))
}
}
}
extension WebView {
class ViewModel: ObservableObject {
#Published var url: String
init(url: String) {
self.url = url
}
}
}
struct ContentView: View {
#State private var currentWebsite = "https://google.com"
var body: some View {
VStack {
Button("Change") {
self.currentWebsite = "https://apple.com"
}
WebView(viewModel: .init(url: currentWebsite))
}
}
}

SwiftUI View not updating when Published variable is changed

On a button press, I my app is trying to contact an api to receive data. This data is then stored in a published variable inside an observable object. For some reason, the view doesn't populate with the data until the button that opens that view is press more than once. I am looking for the view to update with the information received from the api call on the first button press. The code I am referencing is provided below:
DataFetcher.swift:
class DataFetcher: ObservableObject{
#Published var dataHasLoaded: Bool = false
#Published var attendeesLoaded: Bool = false
#Published var useresUventsLoaded: Bool = false
#Published var profilesLoaded: Bool = false
#Published var eventsUpdated: Bool = false
#Published var events: [eventdata] = []
#Published var createdEvents: [eventdata] = []
#Published var profile: profiledata?
#Published var atendees: [atendeedata] = []
#Published var IAmAtending: [atendeedata] = []
#Published var eventNames: [eventdata] = []
#Published var profileList: [profiledata] = []
#Published var token: String = UserDefaults.standard.string(forKey: "Token") ?? ""
private var id: Int = 0
func fetchProfile(id: Int){
// events.removeAll()
profileUrl.append("/\(id)")
self.id = id
let url = URL(string: profileUrl)!
var request = URLRequest(url: url)
if let range = profileUrl.range(of: "/\(id)") {
profileUrl.removeSubrange(range)
}
request.httpMethod = "GET"
print(self.token)
request.addValue("Token \(self.token)", forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request, completionHandler: parseFetchProfileObject)
task.resume()
}
func parseFetchProfileObject(data: Data?, urlResponse: URLResponse?, error: Error?){
guard error == nil else {
print("\(error!)")
return
}
guard let content = data else{
print("No data")
return
}
if let decodedResponse = try? JSONDecoder().decode(profiledata?.self, from: content) {
DispatchQueue.main.async {
self.profile = decodedResponse
self.profileList.append(self.profile!)
}
}
}
func fetchAtendees(id: Int){
// events.removeAll()
atendeeUrl.append("/\(id)")
print(atendeeUrl)
let url = URL(string: atendeeUrl)!
var request = URLRequest(url: url)
if let range = atendeeUrl.range(of:"/\(id)") {
atendeeUrl.removeSubrange(range)
}
request.httpMethod = "GET"
print(self.token)
request.addValue("Token \(self.token)", forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request, completionHandler: parseFetchAttendeesObject)
task.resume()
}
EventsUserCreatedView.swift
import Foundation
import SwiftUI
import Mapbox
struct EventsUserCreatedView: View {
#Binding var token: String
#State private var pressedEvent: Bool = false
#State private var selectedEvent: Int = 0
#State private var atendees: [atendeedata] = []
#State private var profileList: [profiledata] = []
#State private var showDeleteEventView: Bool = false
var data: DataFetcher
var mapStyle: URL
var body: some View {
ZStack{
//NavigationView {
if self.mapStyle == MGLStyle.darkStyleURL {
List{
ForEach(self.data.createdEvents){ row in
HStack {
Button("\((row.poi)!)") {
print("Display event information")
self.selectedEvent = row.id
self.pressedEvent = true
}
Spacer()
Button("Delete") {
self.showDeleteEventView = true
print("Deletes the event in this row")
}.buttonStyle(BorderlessButtonStyle())
.padding(4)
.background(Color.red)
.cornerRadius(5)
}.foregroundColor(Color.white)
}
}.background(Color.init(red: 0.05, green: 0.05, blue: 0.05))
//if you hit more buttons than there is room for, it'll be scrollable. make some kind of for loop that iterates over events a user is going to and displays it
// }.navigationBarTitle("My Events")
// .navigationViewStyle(StackNavigationViewStyle())
if pressedEvent{
Group{
if self.data.profilesLoaded == true{
//NavigationView {
List{
ForEach(self.data.profileList){ row in
HStack {
Text("\(row.name)")
.foregroundColor(Color.purple)
Spacer()
}
}
}.background(Color.init(red: 0.05, green: 0.05, blue: 0.05))
//if you hit more buttons than there is room for, it'll be scrollable. make some kind of for loop that iterates over events a user is going to and displays it
//}
} else{
Spacer()
Text("Loading Attendees")
Spacer()
}
}.onAppear{
//this can't be done on appear as it won't update when a different
self.data.profileList = []
self.data.atendees = []
DispatchQueue.main.async{
self.data.fetchAtendees(id: self.selectedEvent)
if self.data.profilesLoaded{
self.profileList = self.data.profileList
self.atendees = self.data.atendees
}
}
}
//.navigationBarTitle("My Attendees")
//.navigationViewStyle(StackNavigationViewStyle())
}
NOTE: datafetcher (the observableobject) is passed to eventsusercreated view by the contentview
any help on how to update my view properly is much appreciated
You've to declare the data as an #ObservedObject.
struct EventsUserCreatedView: View {
//...
#ObservedObject var data = DataFetcher()
//...
}
If you're passing DataFetcher instance as environment object declare it as #EnvironmentObject.
struct EventsUserCreatedView: View {
//...
#EnvironmentObject var data: DataFetcher
//...
}

Resources