Content view is not updating when the variable is changed - ios

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))
}
}
}

Related

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

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.

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 ?? "-")
}
}

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") }
}
}
}

SafariView only loads a single url, can't seem to load another

I want to load specific webpages inside a Safari browser INSIDE the app (i.e. not going outside the app), and it should exist within the same safari-environment, i.e. no regular webviews.
I have this SafariView to enable that in SwiftUI.
Now I want to load different urls from the same scene (The number varies, can be 0 to 20-ish).
When I open the SafariViews though only the first url is opened. When I click the second button the first url is loaded again.
import SwiftUI
import SafariServices
struct ContentView: View {
#State private var showSafari = false
var body: some View {
VStack {
Button(action: {
showSafari = true
}) {
Text("Apple")
.padding()
}
.fullScreenCover(isPresented: $showSafari) {
SafariView(url: URL(string: "http://www.apple.com")!)
}
Button(action: {
showSafari = true
}) {
Text("Google")
.padding()
}
.fullScreenCover(isPresented: $showSafari) {
SafariView(url: URL(string: "http://www.google.com")!)
}
}
}
}
struct SafariView: UIViewControllerRepresentable {
var url: URL
func makeUIViewController(
context: UIViewControllerRepresentableContext<SafariView>
) -> SFSafariViewController {
return SFSafariViewController(url: url)
}
func updateUIViewController(
_ uiViewController: SFSafariViewController,
context: UIViewControllerRepresentableContext<SafariView>
) {}
}
What I am doing in another scene is creating 2 separate showSafari variables, there it seems to work, but in that case it is only ever 2 hard-coded urls being shown.
Is there something I am missing in this safari-implementation, or do I possibly need to work around this by creating an array of showSafari booleans?
Try using .fullScreenCover(item:content:):
struct ContentView: View {
#State private var safariURL: String?
var body: some View {
VStack {
Button(action: {
safariURL = "http://www.apple.com"
}) {
Text("Apple")
.padding()
}
Button(action: {
safariURL = "http://www.google.com"
}) {
Text("Google")
.padding()
}
}
.fullScreenCover(item: $safariURL) {
if let url = URL(string: $0) {
SafariView(url: url)
}
}
}
}
Note that you need to pass some Identifiable variable in item. A possible solution is to conform String to Identifiable:
extension String: Identifiable {
public var id: Self { self }
}

Deep link onChange not triggered in SwiftUI sheet

I have an issue with deep links in my SwiftUI app.
In my app class I have declared deepLink as an environment variable for every View under ContentView() in the hierarchy:
...
#main
struct TestApp: App {
var userSettings: UserSettings
var dataFetcher: DataFetcher
var dataUpdater: DataUpdater
#State var deepLink = ""
init() {
userSettings = UserSettings()
dataFetcher = DataFetcher(userSettings: userSettings)
dataUpdater = DataUpdater(userSettings: userSettings)
}
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(userSettings)
.environmentObject(dataFetcher)
.environmentObject(dataUpdater)
.onOpenURL { url in
deepLink = url.absoluteString
}
.environment(\.deepLink, deepLink)
}
}
}
In my ContentView() I've declared deepLink as an environment variable
struct ContentView: View {
...
#State var isTestSheetViewPresented = false
#Environment(\.deepLink) var deepLink: String
...
var body: some View {
Button(action: {
self.isTestSheetViewPresented = true
}, label: {
HStack {
Spacer()
Image(systemName: "plus")
Text("Add")
Spacer()
}
})
.sheet(isPresented: $isTestSheetViewPresented, content: {
TestSheetView(isPresented: self.$isTestSheetViewPresented)
})
.onChange(of: self.deepLink) { _ in
self.isTestSheetViewPresented = true
}
}
}
And the TestSheetView is like this
struct TestSheetView: View {
#Environment(\.deepLink) var deepLink: String
#State var url: String = ""
var body: some View {
Text(url)
.onChange(of: deepLink) { _ in
if deepLink != "" {
self.url = deepLink
}
}
}
}
The problem is that when I click on a link, and my app opens, the TestSheetView is correctly presented but the onChange is not triggered unless I scroll a little bit down the sheet.
Instead if I put the same code of the TestSheetView in the ContentView then the text is correctly shown
Seems like a timing issue. While TestSheetView is being initialized, the body is created after deepLink changed, so it won't be able to detect it.
The solution is to use onAppear in TestSheetView and read from there, like so:
struct TestSheetView: View {
#Environment(\.deepLink) var deepLink: String
#State var url: String = ""
var body: some View {
Text(url)
.onAppear {
if deepLink != "" {
self.url = deepLink
}
}
}
}
It's unergonomic to handle both the case where the view is yet to appear, and the case where a link is being navigated within the view. The following view modifier handles both cases. It assumes an .onOpenURL() handler in the top level navigating view that sets both the current tab selection, along with the currentDeepLink environment value.
struct DeepLinkViewModifier: ViewModifier {
#Environment(\.currentDeepLink) private var currentDeepLink
let action: ((URL) -> Bool)
func body(content: Content) -> some View {
content
.onAppear {
if let url = currentDeepLink.wrappedValue,
action(url) {
currentDeepLink.wrappedValue = nil
}
}
.onOpenURL { url in
_ = action(url)
}
}
}
extension View {
func onDeepLink(perform action: #escaping ((URL) -> Bool)) -> some View {
return self.modifier(DeepLinkViewModifier(action: action))
}
}
Use:
struct SomeView: View {
#State urlString: String = ""
var body: some View {
Text(urlString).onDeepLink { url
self.string = url.absoluteString
return true // return false if another handler should consume
}
}
}
struct ContentView: View {
#State private var tabSelection: TabSelection = .something
#State private var currentDeepLink: URL? = nil
var body: some View {
TabView(selection: self.$tabSelection) {
...
}
.onOpenURL { url in
self.tabSelection = ... // determine selection from URL
self.currentDeepLink = url
}
.environment(\.tabSelection, self.$tabSelection)
.environment(\.currentDeepLink, self.$currentDeepLink)
}
}

Resources