From 6134fa8ae68b8c0470adb7e7326cdbf3c6129a89 Mon Sep 17 00:00:00 2001 From: Renato Ferraz Date: Sat, 13 Dec 2025 12:40:12 -0300 Subject: [PATCH 1/4] Adds Instagram feed to social section Implements a WebView-based Instagram feed in the social section. This feature provides users with access to the Instagram feed within the app. It handles loading states and errors gracefully, displaying a placeholder view when content is unavailable. It also configures the scroll behavior based on device type to correctly display the content on both iPad and iPhone. Furthermore, the feature enables opening Instagram URLs, either within the app or by redirecting to the Instagram app, if installed. The feature is behind a check to ensure if the content is available. --- .../SidebarVisibilityKey.swift | 12 + .../Social/InstagramContainerView.swift | 60 ++++ .../Social/InstagramPostsWebView.swift | 273 ++++++++++++++++++ .../Features/Social/SocialView.swift | 25 +- .../MacMagazine/MainApp/MainView.swift | 1 + .../MainApp/Sizecalss/MainView+sidebar.swift | 6 +- .../Modifiers/NavigationModifier.swift | 8 +- MacMagazine/MacMagazine/Resources/Info.plist | 4 + 8 files changed, 381 insertions(+), 8 deletions(-) create mode 100644 MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/SidebarVisibilityKey.swift create mode 100644 MacMagazine/MacMagazine/Features/Social/InstagramContainerView.swift create mode 100644 MacMagazine/MacMagazine/Features/Social/InstagramPostsWebView.swift diff --git a/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/SidebarVisibilityKey.swift b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/SidebarVisibilityKey.swift new file mode 100644 index 00000000..0235fa0b --- /dev/null +++ b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/SidebarVisibilityKey.swift @@ -0,0 +1,12 @@ +import SwiftUI + +public struct SidebarVisibilityKey: EnvironmentKey { + public static let defaultValue: Bool = false +} + +public extension EnvironmentValues { + var isSidebarVisible: Bool { + get { self[SidebarVisibilityKey.self] } + set { self[SidebarVisibilityKey.self] = newValue } + } +} diff --git a/MacMagazine/MacMagazine/Features/Social/InstagramContainerView.swift b/MacMagazine/MacMagazine/Features/Social/InstagramContainerView.swift new file mode 100644 index 00000000..ae797425 --- /dev/null +++ b/MacMagazine/MacMagazine/Features/Social/InstagramContainerView.swift @@ -0,0 +1,60 @@ +import SwiftUI + +struct InstagramContainerView: View { + let url: URL + let userAgent: String + let shouldUseSidebar: Bool + + @State private var status: Status = .checking + + enum Status: Equatable { + case checking + case available + case unavailable + } + + var body: some View { + Group { + switch status { + case .checking: + ProgressView() + .task { await checkAvailability() } + + case .available: + InstagramPostsWebView( + url: url, + userAgent: userAgent, + shouldUseSidebar: shouldUseSidebar + ) + + case .unavailable: + ContentUnavailableView( + "Estamos com um problema", + systemImage: "square.and.arrow.down.badge.xmark", + description: Text( + "No momento estamos com um problema técnico. Tente novamente mais tarde." + ) + ) + } + } + } + + private func checkAvailability() async { + do { + var request = URLRequest(url: url) + request.httpMethod = "HEAD" + request.timeoutInterval = 8 + + let (_, response) = try await URLSession.shared.data(for: request) + let code = (response as? HTTPURLResponse)?.statusCode ?? 0 + + if (200...399).contains(code) { + status = .available + } else { + status = .unavailable + } + } catch { + status = .unavailable + } + } +} diff --git a/MacMagazine/MacMagazine/Features/Social/InstagramPostsWebView.swift b/MacMagazine/MacMagazine/Features/Social/InstagramPostsWebView.swift new file mode 100644 index 00000000..fec9d719 --- /dev/null +++ b/MacMagazine/MacMagazine/Features/Social/InstagramPostsWebView.swift @@ -0,0 +1,273 @@ +import MacMagazineLibrary +import SwiftUI +import UIComponentsLibrary +import WebKit + +struct InstagramPostsWebView: View { + let url: URL + let userAgent: String + let shouldUseSidebar: Bool + + @Environment(\.theme) private var theme: ThemeColor + @Environment(\.scenePhase) private var scenePhase + @Environment(\.colorScheme) private var colorScheme + + @State private var refreshKey = 0 + @State private var isLoading = false + @State private var loadError: String? + @State private var reloadToken = UUID() + + var body: some View { + GeometryReader { proxy in + let background = theme.main.background.color ?? Color.secondary + let safeTop = proxy.safeAreaInsets.top + + ZStack { + background.ignoresSafeArea() + + let mode: WebViewRepresentable.Mode = shouldUseSidebar + ? .manualInsets( + topContentInset: safeTop, + topIndicatorInset: safeTop + ) + + : .manualInsets( + topContentInset: safeTop, + topIndicatorInset: safeTop - 90 + ) + + WebViewRepresentable( + url: url, + userAgent: userAgent, + backgroundColor: background, + mode: mode, + refreshKey: refreshKey, + isLoading: $isLoading, + loadError: $loadError + ) + .id(reloadToken) + .ignoresSafeArea(.container, edges: [.top, .bottom]) + .onChange(of: scenePhase) { _, newValue in + if newValue == .active { refreshKey += 1 } + } + .onChange(of: colorScheme) { _, _ in + refreshKey += 1 + } + + if isLoading { + ProgressView() + } + if loadError != nil { + VStack(spacing: 12) { + ContentUnavailableView( + "Estamos com um problema", + systemImage: "square.and.arrow.down.badge.xmark", + description: Text( + "No momento estamos com um problema técnico. Tente novamente mais tarde." + ) + ) + + Button("Recarregar") { + loadError = nil + reloadToken = UUID() + } + } + .padding() + .clipShape(RoundedRectangle(cornerRadius: 16)) + .padding() + } + } + } + } +} + + // MARK: - UIViewRepresentable + +private struct WebViewRepresentable: UIViewRepresentable { + enum Mode: Equatable { + case systemManaged + case manualInsets(topContentInset: CGFloat, topIndicatorInset: CGFloat) + } + + let url: URL + let userAgent: String + let backgroundColor: Color + let mode: Mode + let refreshKey: Int + + @Binding var isLoading: Bool + @Binding var loadError: String? + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + func makeUIView(context: Context) -> WKWebView { + let config = WKWebViewConfiguration() + + let javascript = """ + (function() { + var s = document.createElement('style'); + s.innerHTML = 'html,body{background: transparent !important;}'; + document.head.appendChild(s); + })(); + """ + let script = WKUserScript( + source: javascript, + injectionTime: .atDocumentEnd, + forMainFrameOnly: true + ) + config.userContentController.addUserScript(script) + + let webView = WKWebView(frame: .zero, configuration: config) + + webView.isOpaque = false + webView.backgroundColor = .clear + + webView.navigationDelegate = context.coordinator + webView.allowsBackForwardNavigationGestures = true + webView.customUserAgent = userAgent + + // Configura scroll conforme o modo (iPad vs iPhone) + configureScrollBehavior(webView, coordinator: context.coordinator) + applyBackground(to: webView) + + webView.load(URLRequest(url: url)) + return webView + } + + func updateUIView(_ webView: WKWebView, context: Context) { + // refreshKey serve para forçar update quando app volta ativo / troca colorScheme + _ = refreshKey + + configureScrollBehavior(webView, coordinator: context.coordinator) + applyBackground(to: webView) + + if webView.url != url { + webView.load(URLRequest(url: url)) + } + } + + private func configureScrollBehavior(_ webView: WKWebView, coordinator: Coordinator) { + switch mode { + case .systemManaged: + webView.scrollView.contentInsetAdjustmentBehavior = .automatic + + // importantíssimo: não carregar herança do modo manual + webView.scrollView.contentInset = .zero + webView.scrollView.verticalScrollIndicatorInsets = .zero + webView.scrollView.horizontalScrollIndicatorInsets = .zero + + coordinator.didApplyInitialOffset = true + case let .manualInsets(topContentInset, topIndicatorInset): + webView.scrollView.contentInsetAdjustmentBehavior = .never + webView.scrollView.verticalScrollIndicatorInsets.bottom = 12 + + var inset = webView.scrollView.contentInset + inset.top = topContentInset + webView.scrollView.contentInset = inset + + webView.scrollView.verticalScrollIndicatorInsets.top = topIndicatorInset + + if coordinator.didApplyInitialOffset == false { + coordinator.didApplyInitialOffset = true + webView.scrollView.setContentOffset( + CGPoint(x: 0, y: -topContentInset), + animated: false + ) + } + } + } + + private func applyBackground(to webView: WKWebView) { + let base = UIColor(backgroundColor) + let resolved = base.resolvedColor(with: webView.traitCollection) + + webView.scrollView.backgroundColor = resolved + webView.underPageBackgroundColor = resolved + } + + final class Coordinator: NSObject, WKNavigationDelegate { + private let parent: WebViewRepresentable + var didApplyInitialOffset = false + + init(_ parent: WebViewRepresentable) { + self.parent = parent + } + + func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation?) { + parent.loadError = nil + parent.isLoading = true + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation?) { + parent.isLoading = false + } + + func webView( + _ webView: WKWebView, + didFailProvisionalNavigation navigation: WKNavigation?, + withError error: Error + ) { + parent.isLoading = false + parent.loadError = error.localizedDescription + } + + func webView(_ webView: WKWebView, didFail navigation: WKNavigation?, withError error: Error) { + parent.isLoading = false + parent.loadError = error.localizedDescription + } + + func webView( + _ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void + ) { + guard let targetURL = navigationAction.request.url else { + decisionHandler(.cancel) + return + } + + guard navigationAction.navigationType == .linkActivated else { + decisionHandler(.allow) + return + } + + if navigationAction.targetFrame?.isMainFrame == false { + decisionHandler(.allow) + return + } + + if isInstagramURL(targetURL) { + openInstagramOrFallback(targetURL) + decisionHandler(.cancel) + return + } + + decisionHandler(.allow) + } + + private func isInstagramURL(_ url: URL) -> Bool { + guard let host = url.host?.lowercased() else { return false } + return host.contains("instagram.com") + } + + private func openInstagramOrFallback(_ webURL: URL) { + if let appURL = makeInstagramAppURL(from: webURL), + UIApplication.shared.canOpenURL(appURL) { + UIApplication.shared.open(appURL) + return + } + + UIApplication.shared.open(webURL) + } + + private func makeInstagramAppURL(from webURL: URL) -> URL? { + var components = URLComponents(url: webURL, resolvingAgainstBaseURL: false) + components?.scheme = "instagram" + components?.host = nil + return components?.url + } + } +} + diff --git a/MacMagazine/MacMagazine/Features/Social/SocialView.swift b/MacMagazine/MacMagazine/Features/Social/SocialView.swift index f72780ea..44b7612e 100644 --- a/MacMagazine/MacMagazine/Features/Social/SocialView.swift +++ b/MacMagazine/MacMagazine/Features/Social/SocialView.swift @@ -32,7 +32,8 @@ struct SocialView: View { content } .contentMargins(.top, 20, for: .scrollContent) - .navigation(shouldUseSidebar: shouldUseSidebar, title: viewModel.social.rawValue) + .navigation(shouldUseSidebar: shouldUseSidebar, + title: viewModel.social.rawValue) .toolbar { ToolbarItem(placement: .primaryAction) { menuView @@ -75,11 +76,23 @@ private extension SocialView { scrollPosition: $scrollPosition ).transition(.opacity) case .instagram: - ContentUnavailableView( - "Página em construção", - systemImage: "square.and.arrow.down.badge.xmark", - description: Text("Conteúdo ainda em desenvolvimento e estará disponível em breve.") - ) + if let url = URL(string: "https://macmagazine.com.br/posts-instagram-app/") { + InstagramContainerView( + url: url, + userAgent: "MacMagazine", + shouldUseSidebar: shouldUseSidebar + ) + .transition(.opacity) + } else { + ContentUnavailableView( + "Estamos com um problema", + systemImage: "square.and.arrow.down.badge.xmark", + description: Text( + "No momento estamos com um problema técnico. Tente novamente mais tarde." + ) + ) + .transition(.opacity) + } } } diff --git a/MacMagazine/MacMagazine/MainApp/MainView.swift b/MacMagazine/MacMagazine/MainApp/MainView.swift index 5abdf041..9b2a3a4a 100644 --- a/MacMagazine/MacMagazine/MainApp/MainView.swift +++ b/MacMagazine/MacMagazine/MainApp/MainView.swift @@ -13,6 +13,7 @@ struct MainView: View { @Environment(PodcastPlayerManager.self) private var podcastManager @State var searchText: String = "" + @State var splitViewVisibility: NavigationSplitViewVisibility = .all var body: some View { @Bindable var bindableViewModel = viewModel diff --git a/MacMagazine/MacMagazine/MainApp/Sizecalss/MainView+sidebar.swift b/MacMagazine/MacMagazine/MainApp/Sizecalss/MainView+sidebar.swift index 97047916..09756243 100644 --- a/MacMagazine/MacMagazine/MainApp/Sizecalss/MainView+sidebar.swift +++ b/MacMagazine/MacMagazine/MainApp/Sizecalss/MainView+sidebar.swift @@ -5,11 +5,15 @@ import SwiftUI extension MainView { var sideBarContentView: some View { - NavigationSplitView { + let isSidebarVisible = + splitViewVisibility == .all || splitViewVisibility == .doubleColumn + + return NavigationSplitView(columnVisibility: $splitViewVisibility) { sidebar .searchable(text: $searchText, prompt: "Search items") } detail: { animateContentStackView(for: navigationState.selectedItem) + .environment(\.isSidebarVisible, isSidebarVisible) .podcastMiniPlayer { if let social = navigationState.selectedItem as? Social { return social == .podcast diff --git a/MacMagazine/MacMagazine/Modifiers/NavigationModifier.swift b/MacMagazine/MacMagazine/Modifiers/NavigationModifier.swift index 5f65aa77..9dd92a19 100644 --- a/MacMagazine/MacMagazine/Modifiers/NavigationModifier.swift +++ b/MacMagazine/MacMagazine/Modifiers/NavigationModifier.swift @@ -1,7 +1,10 @@ import SwiftUI extension View { - func navigation(shouldUseSidebar: Bool, title: String? = nil) -> some View { + func navigation( + shouldUseSidebar: Bool, + title: String? = nil + ) -> some View { modifier(NavigationModifier( shouldUseSidebar: shouldUseSidebar, title: title @@ -13,11 +16,14 @@ private struct NavigationModifier: ViewModifier { let shouldUseSidebar: Bool let title: String? + @Environment(\.isSidebarVisible) private var isSidebarVisible + func body(content: Content) -> some View { if shouldUseSidebar { if let title { content .navigationTitle(title) + .navigationBarTitleDisplayMode(isSidebarVisible ? .inline : .large ) } else { content } diff --git a/MacMagazine/MacMagazine/Resources/Info.plist b/MacMagazine/MacMagazine/Resources/Info.plist index 656a9c28..dfc55d0e 100644 --- a/MacMagazine/MacMagazine/Resources/Info.plist +++ b/MacMagazine/MacMagazine/Resources/Info.plist @@ -8,5 +8,9 @@ audio fetch + LSApplicationQueriesSchemes + + instagram + From 2e1bdf0655cf25511018a2be4bc577e676c157f6 Mon Sep 17 00:00:00 2001 From: Renato Ferraz Date: Mon, 15 Dec 2025 17:47:42 -0300 Subject: [PATCH 2/4] Refactors Instagram integration to use WebView Replaces the previous Instagram integration with a `WebView` to improve stability and address potential issues with the older implementation. The `InstagramContainerView` is removed and `SocialView` is updated to use the new `InstagramPostsWebView` directly. This change ensures a more consistent and reliable user experience when viewing Instagram content within the app. The `MMGrey6` color definition is updated to use explicit srgb color values instead of a system reference. --- .../Colors/MMGrey6.colorset/Contents.json | 9 +- .../Social/InstagramContainerView.swift | 60 ---- .../Social/InstagramPostsWebView.swift | 335 ++++++------------ .../Features/Social/SocialView.swift | 2 +- .../Modifiers/NavigationModifier.swift | 2 +- 5 files changed, 115 insertions(+), 293 deletions(-) delete mode 100644 MacMagazine/MacMagazine/Features/Social/InstagramContainerView.swift diff --git a/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Resources/Media.xcassets/Colors/MMGrey6.colorset/Contents.json b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Resources/Media.xcassets/Colors/MMGrey6.colorset/Contents.json index dc3e3fc4..3b1e3862 100644 --- a/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Resources/Media.xcassets/Colors/MMGrey6.colorset/Contents.json +++ b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Resources/Media.xcassets/Colors/MMGrey6.colorset/Contents.json @@ -2,8 +2,13 @@ "colors" : [ { "color" : { - "platform" : "ios", - "reference" : "systemGray6Color" + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.969", + "green" : "0.949", + "red" : "0.949" + } }, "idiom" : "iphone" }, diff --git a/MacMagazine/MacMagazine/Features/Social/InstagramContainerView.swift b/MacMagazine/MacMagazine/Features/Social/InstagramContainerView.swift deleted file mode 100644 index ae797425..00000000 --- a/MacMagazine/MacMagazine/Features/Social/InstagramContainerView.swift +++ /dev/null @@ -1,60 +0,0 @@ -import SwiftUI - -struct InstagramContainerView: View { - let url: URL - let userAgent: String - let shouldUseSidebar: Bool - - @State private var status: Status = .checking - - enum Status: Equatable { - case checking - case available - case unavailable - } - - var body: some View { - Group { - switch status { - case .checking: - ProgressView() - .task { await checkAvailability() } - - case .available: - InstagramPostsWebView( - url: url, - userAgent: userAgent, - shouldUseSidebar: shouldUseSidebar - ) - - case .unavailable: - ContentUnavailableView( - "Estamos com um problema", - systemImage: "square.and.arrow.down.badge.xmark", - description: Text( - "No momento estamos com um problema técnico. Tente novamente mais tarde." - ) - ) - } - } - } - - private func checkAvailability() async { - do { - var request = URLRequest(url: url) - request.httpMethod = "HEAD" - request.timeoutInterval = 8 - - let (_, response) = try await URLSession.shared.data(for: request) - let code = (response as? HTTPURLResponse)?.statusCode ?? 0 - - if (200...399).contains(code) { - status = .available - } else { - status = .unavailable - } - } catch { - status = .unavailable - } - } -} diff --git a/MacMagazine/MacMagazine/Features/Social/InstagramPostsWebView.swift b/MacMagazine/MacMagazine/Features/Social/InstagramPostsWebView.swift index fec9d719..c4121f5b 100644 --- a/MacMagazine/MacMagazine/Features/Social/InstagramPostsWebView.swift +++ b/MacMagazine/MacMagazine/Features/Social/InstagramPostsWebView.swift @@ -9,265 +9,142 @@ struct InstagramPostsWebView: View { let shouldUseSidebar: Bool @Environment(\.theme) private var theme: ThemeColor - @Environment(\.scenePhase) private var scenePhase - @Environment(\.colorScheme) private var colorScheme - @State private var refreshKey = 0 - @State private var isLoading = false + @State private var isPresenting = true + @State private var isLoading = true @State private var loadError: String? - @State private var reloadToken = UUID() - var body: some View { - GeometryReader { proxy in - let background = theme.main.background.color ?? Color.secondary - let safeTop = proxy.safeAreaInsets.top - - ZStack { - background.ignoresSafeArea() - - let mode: WebViewRepresentable.Mode = shouldUseSidebar - ? .manualInsets( - topContentInset: safeTop, - topIndicatorInset: safeTop - ) + private let navigationDelegate = InstagramNavigationDelegate() + + private var userScripts: [WKUserScript] { + [ + WKUserScript( + source: """ + (function() { + var style = document.createElement('style'); + style.innerHTML = ` + html, body { + padding-top: 50px !important; + background-color: transparent !important; + } + + @media (prefers-color-scheme: dark) { + html, body { background-color: #1C1B1D !important; } + } + + @media (prefers-color-scheme: light) { + html, body { background-color: #F2F2F7 !important; } + } + `; + document.head.appendChild(style); + })(); + """, + injectionTime: .atDocumentEnd, + forMainFrameOnly: true + ) + ] + } - : .manualInsets( - topContentInset: safeTop, - topIndicatorInset: safeTop - 90 - ) + var body: some View { + ZStack { + (theme.main.background.color ?? Color.secondary) + .ignoresSafeArea() + + Webview( + title: nil, + url: url.absoluteString, + isPresenting: $isPresenting, + standAlone: true, + navigationDelegate: navigationDelegate, + userScripts: userScripts, + userAgent: userAgent + ) + .ignoresSafeArea(.container, edges: [.top, .bottom]) + .opacity(isLoading ? 0 : 1) + .animation(.easeInOut(duration: 0.25), value: isLoading) + + if isLoading { + ProgressView() + .transition(.opacity) + } - WebViewRepresentable( - url: url, - userAgent: userAgent, - backgroundColor: background, - mode: mode, - refreshKey: refreshKey, - isLoading: $isLoading, - loadError: $loadError + if loadError != nil { + ContentUnavailableView( + "Estamos com um problema", + systemImage: "square.and.arrow.down.badge.xmark", + description: Text("No momento estamos com um problema técnico. Tente novamente mais tarde.") ) - .id(reloadToken) - .ignoresSafeArea(.container, edges: [.top, .bottom]) - .onChange(of: scenePhase) { _, newValue in - if newValue == .active { refreshKey += 1 } - } - .onChange(of: colorScheme) { _, _ in - refreshKey += 1 - } - - if isLoading { - ProgressView() - } - if loadError != nil { - VStack(spacing: 12) { - ContentUnavailableView( - "Estamos com um problema", - systemImage: "square.and.arrow.down.badge.xmark", - description: Text( - "No momento estamos com um problema técnico. Tente novamente mais tarde." - ) - ) - - Button("Recarregar") { - loadError = nil - reloadToken = UUID() - } - } - .padding() - .clipShape(RoundedRectangle(cornerRadius: 16)) - .padding() + } + } + .onAppear { + navigationDelegate.onStart = { isLoading = true; loadError = nil } + navigationDelegate.onFinish = { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + isLoading = false } } + navigationDelegate.onFail = { error in + isLoading = false + loadError = error.localizedDescription + } } } } - // MARK: - UIViewRepresentable +final class InstagramNavigationDelegate: NSObject, WKNavigationDelegate { + var onStart: (() -> Void)? + var onFinish: (() -> Void)? + var onFail: ((Error) -> Void)? -private struct WebViewRepresentable: UIViewRepresentable { - enum Mode: Equatable { - case systemManaged - case manualInsets(topContentInset: CGFloat, topIndicatorInset: CGFloat) + func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation?) { + onStart?() } - let url: URL - let userAgent: String - let backgroundColor: Color - let mode: Mode - let refreshKey: Int - - @Binding var isLoading: Bool - @Binding var loadError: String? - - func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - func makeUIView(context: Context) -> WKWebView { - let config = WKWebViewConfiguration() - - let javascript = """ - (function() { - var s = document.createElement('style'); - s.innerHTML = 'html,body{background: transparent !important;}'; - document.head.appendChild(s); - })(); - """ - let script = WKUserScript( - source: javascript, - injectionTime: .atDocumentEnd, - forMainFrameOnly: true - ) - config.userContentController.addUserScript(script) - - let webView = WKWebView(frame: .zero, configuration: config) - - webView.isOpaque = false - webView.backgroundColor = .clear - - webView.navigationDelegate = context.coordinator - webView.allowsBackForwardNavigationGestures = true - webView.customUserAgent = userAgent - - // Configura scroll conforme o modo (iPad vs iPhone) - configureScrollBehavior(webView, coordinator: context.coordinator) - applyBackground(to: webView) - - webView.load(URLRequest(url: url)) - return webView - } - - func updateUIView(_ webView: WKWebView, context: Context) { - // refreshKey serve para forçar update quando app volta ativo / troca colorScheme - _ = refreshKey - - configureScrollBehavior(webView, coordinator: context.coordinator) - applyBackground(to: webView) - - if webView.url != url { - webView.load(URLRequest(url: url)) - } + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation?) { + onFinish?() } - private func configureScrollBehavior(_ webView: WKWebView, coordinator: Coordinator) { - switch mode { - case .systemManaged: - webView.scrollView.contentInsetAdjustmentBehavior = .automatic - - // importantíssimo: não carregar herança do modo manual - webView.scrollView.contentInset = .zero - webView.scrollView.verticalScrollIndicatorInsets = .zero - webView.scrollView.horizontalScrollIndicatorInsets = .zero - - coordinator.didApplyInitialOffset = true - case let .manualInsets(topContentInset, topIndicatorInset): - webView.scrollView.contentInsetAdjustmentBehavior = .never - webView.scrollView.verticalScrollIndicatorInsets.bottom = 12 - - var inset = webView.scrollView.contentInset - inset.top = topContentInset - webView.scrollView.contentInset = inset - - webView.scrollView.verticalScrollIndicatorInsets.top = topIndicatorInset - - if coordinator.didApplyInitialOffset == false { - coordinator.didApplyInitialOffset = true - webView.scrollView.setContentOffset( - CGPoint(x: 0, y: -topContentInset), - animated: false - ) - } - } + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation?, withError error: Error) { + onFail?(error) } - private func applyBackground(to webView: WKWebView) { - let base = UIColor(backgroundColor) - let resolved = base.resolvedColor(with: webView.traitCollection) - - webView.scrollView.backgroundColor = resolved - webView.underPageBackgroundColor = resolved + func webView(_ webView: WKWebView, didFail navigation: WKNavigation?, withError error: Error) { + onFail?(error) } - final class Coordinator: NSObject, WKNavigationDelegate { - private let parent: WebViewRepresentable - var didApplyInitialOffset = false - - init(_ parent: WebViewRepresentable) { - self.parent = parent - } - - func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation?) { - parent.loadError = nil - parent.isLoading = true - } - - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation?) { - parent.isLoading = false - } - - func webView( - _ webView: WKWebView, - didFailProvisionalNavigation navigation: WKNavigation?, - withError error: Error - ) { - parent.isLoading = false - parent.loadError = error.localizedDescription + func webView( + _ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void + ) { + guard let url = navigationAction.request.url else { + decisionHandler(.cancel) + return } - func webView(_ webView: WKWebView, didFail navigation: WKNavigation?, withError error: Error) { - parent.isLoading = false - parent.loadError = error.localizedDescription - } - - func webView( - _ webView: WKWebView, - decidePolicyFor navigationAction: WKNavigationAction, - decisionHandler: @escaping (WKNavigationActionPolicy) -> Void - ) { - guard let targetURL = navigationAction.request.url else { - decisionHandler(.cancel) - return - } - - guard navigationAction.navigationType == .linkActivated else { - decisionHandler(.allow) - return - } - - if navigationAction.targetFrame?.isMainFrame == false { - decisionHandler(.allow) - return - } - - if isInstagramURL(targetURL) { - openInstagramOrFallback(targetURL) - decisionHandler(.cancel) - return - } - + guard navigationAction.navigationType == .linkActivated else { decisionHandler(.allow) + return } - private func isInstagramURL(_ url: URL) -> Bool { - guard let host = url.host?.lowercased() else { return false } - return host.contains("instagram.com") - } - - private func openInstagramOrFallback(_ webURL: URL) { - if let appURL = makeInstagramAppURL(from: webURL), + if url.host?.lowercased().contains("instagram.com") == true { + if let appURL = makeInstagramAppURL(from: url), UIApplication.shared.canOpenURL(appURL) { UIApplication.shared.open(appURL) - return + } else { + UIApplication.shared.open(url) } - - UIApplication.shared.open(webURL) + decisionHandler(.cancel) + return } - private func makeInstagramAppURL(from webURL: URL) -> URL? { - var components = URLComponents(url: webURL, resolvingAgainstBaseURL: false) - components?.scheme = "instagram" - components?.host = nil - return components?.url - } + decisionHandler(.allow) } -} + private func makeInstagramAppURL(from webURL: URL) -> URL? { + var components = URLComponents(url: webURL, resolvingAgainstBaseURL: false) + components?.scheme = "instagram" + components?.host = nil + return components?.url + } +} diff --git a/MacMagazine/MacMagazine/Features/Social/SocialView.swift b/MacMagazine/MacMagazine/Features/Social/SocialView.swift index 44b7612e..07ce846f 100644 --- a/MacMagazine/MacMagazine/Features/Social/SocialView.swift +++ b/MacMagazine/MacMagazine/Features/Social/SocialView.swift @@ -77,7 +77,7 @@ private extension SocialView { ).transition(.opacity) case .instagram: if let url = URL(string: "https://macmagazine.com.br/posts-instagram-app/") { - InstagramContainerView( + InstagramPostsWebView( url: url, userAgent: "MacMagazine", shouldUseSidebar: shouldUseSidebar diff --git a/MacMagazine/MacMagazine/Modifiers/NavigationModifier.swift b/MacMagazine/MacMagazine/Modifiers/NavigationModifier.swift index 9dd92a19..d64d9f44 100644 --- a/MacMagazine/MacMagazine/Modifiers/NavigationModifier.swift +++ b/MacMagazine/MacMagazine/Modifiers/NavigationModifier.swift @@ -17,7 +17,7 @@ private struct NavigationModifier: ViewModifier { let title: String? @Environment(\.isSidebarVisible) private var isSidebarVisible - + func body(content: Content) -> some View { if shouldUseSidebar { if let title { From 87732e3b3e8990291e03c08be236642be5bfbe27 Mon Sep 17 00:00:00 2001 From: Renato Ferraz Date: Mon, 15 Dec 2025 17:57:20 -0300 Subject: [PATCH 3/4] Uses system color for MMGrey6 color Updates MMGrey6 color to utilize the system provided gray 6 color. This ensures proper appearance in both light and dark modes on iOS. --- .../Media.xcassets/Colors/MMGrey6.colorset/Contents.json | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Resources/Media.xcassets/Colors/MMGrey6.colorset/Contents.json b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Resources/Media.xcassets/Colors/MMGrey6.colorset/Contents.json index 3b1e3862..dc3e3fc4 100644 --- a/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Resources/Media.xcassets/Colors/MMGrey6.colorset/Contents.json +++ b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Resources/Media.xcassets/Colors/MMGrey6.colorset/Contents.json @@ -2,13 +2,8 @@ "colors" : [ { "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.969", - "green" : "0.949", - "red" : "0.949" - } + "platform" : "ios", + "reference" : "systemGray6Color" }, "idiom" : "iphone" }, From 2a765530834b7d7ff90d32d40deeb4afd06d6c7f Mon Sep 17 00:00:00 2001 From: Renato Ferraz Date: Mon, 15 Dec 2025 18:25:11 -0300 Subject: [PATCH 4/4] Refactors Instagram WebView for dark mode support Moves Instagram web view logic to accommodate handling of dark mode settings. This change includes removing the InstagramNavigationDelegate and inlining its logic, adds cookie support for setting dark mode preference. It also adds a new initializer to accept ColorScheme? and determine dark mode. --- .../Social/InstagramNavigationDelegate.swift | 60 ++++++++++ .../Social/InstagramPostsWebView.swift | 105 ++++++++---------- .../Features/Social/SocialView.swift | 1 + 3 files changed, 105 insertions(+), 61 deletions(-) create mode 100644 MacMagazine/MacMagazine/Features/Social/InstagramNavigationDelegate.swift diff --git a/MacMagazine/MacMagazine/Features/Social/InstagramNavigationDelegate.swift b/MacMagazine/MacMagazine/Features/Social/InstagramNavigationDelegate.swift new file mode 100644 index 00000000..76ade5d9 --- /dev/null +++ b/MacMagazine/MacMagazine/Features/Social/InstagramNavigationDelegate.swift @@ -0,0 +1,60 @@ +import SwiftUI +import WebKit + +final class InstagramNavigationDelegate: NSObject, WKNavigationDelegate { + var onStart: (() -> Void)? + var onFinish: (() -> Void)? + var onFail: ((Error) -> Void)? + + func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation?) { + onStart?() + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation?) { + onFinish?() + } + + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation?, withError error: Error) { + onFail?(error) + } + + func webView(_ webView: WKWebView, didFail navigation: WKNavigation?, withError error: Error) { + onFail?(error) + } + + func webView( + _ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void + ) { + guard let url = navigationAction.request.url else { + decisionHandler(.cancel) + return + } + + guard navigationAction.navigationType == .linkActivated else { + decisionHandler(.allow) + return + } + + if url.host?.lowercased().contains("instagram.com") == true { + if let appURL = makeInstagramAppURL(from: url), + UIApplication.shared.canOpenURL(appURL) { + UIApplication.shared.open(appURL) + } else { + UIApplication.shared.open(url) + } + decisionHandler(.cancel) + return + } + + decisionHandler(.allow) + } + + private func makeInstagramAppURL(from webURL: URL) -> URL? { + var components = URLComponents(url: webURL, resolvingAgainstBaseURL: false) + components?.scheme = "instagram" + components?.host = nil + return components?.url + } +} diff --git a/MacMagazine/MacMagazine/Features/Social/InstagramPostsWebView.swift b/MacMagazine/MacMagazine/Features/Social/InstagramPostsWebView.swift index c4121f5b..d9c2c510 100644 --- a/MacMagazine/MacMagazine/Features/Social/InstagramPostsWebView.swift +++ b/MacMagazine/MacMagazine/Features/Social/InstagramPostsWebView.swift @@ -2,12 +2,17 @@ import MacMagazineLibrary import SwiftUI import UIComponentsLibrary import WebKit +#if canImport(UIKit) +import UIKit +#endif struct InstagramPostsWebView: View { let url: URL let userAgent: String let shouldUseSidebar: Bool + private let darkMode: Bool + @Environment(\.theme) private var theme: ThemeColor @State private var isPresenting = true @@ -16,6 +21,23 @@ struct InstagramPostsWebView: View { private let navigationDelegate = InstagramNavigationDelegate() + init( + colorSchema: ColorScheme?, + url: URL, + userAgent: String, + shouldUseSidebar: Bool + ) { + self.url = url + self.userAgent = userAgent + self.shouldUseSidebar = shouldUseSidebar + + self.darkMode = if colorSchema == nil { + Self.isDarkMode() + } else { + colorSchema == .dark + } + } + private var userScripts: [WKUserScript] { [ WKUserScript( @@ -25,15 +47,6 @@ struct InstagramPostsWebView: View { style.innerHTML = ` html, body { padding-top: 50px !important; - background-color: transparent !important; - } - - @media (prefers-color-scheme: dark) { - html, body { background-color: #1C1B1D !important; } - } - - @media (prefers-color-scheme: light) { - html, body { background-color: #F2F2F7 !important; } } `; document.head.appendChild(style); @@ -57,6 +70,7 @@ struct InstagramPostsWebView: View { standAlone: true, navigationDelegate: navigationDelegate, userScripts: userScripts, + cookies: makeCookies(), userAgent: userAgent ) .ignoresSafeArea(.container, edges: [.top, .bottom]) @@ -91,60 +105,29 @@ struct InstagramPostsWebView: View { } } -final class InstagramNavigationDelegate: NSObject, WKNavigationDelegate { - var onStart: (() -> Void)? - var onFinish: (() -> Void)? - var onFail: ((Error) -> Void)? - - func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation?) { - onStart?() - } - - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation?) { - onFinish?() - } - - func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation?, withError error: Error) { - onFail?(error) - } - - func webView(_ webView: WKWebView, didFail navigation: WKNavigation?, withError error: Error) { - onFail?(error) - } - - func webView( - _ webView: WKWebView, - decidePolicyFor navigationAction: WKNavigationAction, - decisionHandler: @escaping (WKNavigationActionPolicy) -> Void - ) { - guard let url = navigationAction.request.url else { - decisionHandler(.cancel) - return +private extension InstagramPostsWebView { + func makeCookies() -> [HTTPCookie]? { + var cookies = [HTTPCookie]() + if let darkMode = Cookies.createDarkMode(darkMode ? "true" : "false") { + cookies.append(darkMode) } - - guard navigationAction.navigationType == .linkActivated else { - decisionHandler(.allow) - return - } - - if url.host?.lowercased().contains("instagram.com") == true { - if let appURL = makeInstagramAppURL(from: url), - UIApplication.shared.canOpenURL(appURL) { - UIApplication.shared.open(appURL) - } else { - UIApplication.shared.open(url) - } - decisionHandler(.cancel) - return - } - - decisionHandler(.allow) + return cookies } +} - private func makeInstagramAppURL(from webURL: URL) -> URL? { - var components = URLComponents(url: webURL, resolvingAgainstBaseURL: false) - components?.scheme = "instagram" - components?.host = nil - return components?.url +#if canImport(UIKit) +private extension InstagramPostsWebView { + static func isDarkMode() -> Bool { + (UIApplication.shared.connectedScenes.first as? UIWindowScene)? + .windows.first? + .rootViewController? + .traitCollection.userInterfaceStyle == .dark + } +} +#else +private extension InstagramPostsWebView { + static func isDarkMode() -> Bool { + false } } +#endif diff --git a/MacMagazine/MacMagazine/Features/Social/SocialView.swift b/MacMagazine/MacMagazine/Features/Social/SocialView.swift index 07ce846f..2352b0d6 100644 --- a/MacMagazine/MacMagazine/Features/Social/SocialView.swift +++ b/MacMagazine/MacMagazine/Features/Social/SocialView.swift @@ -78,6 +78,7 @@ private extension SocialView { case .instagram: if let url = URL(string: "https://macmagazine.com.br/posts-instagram-app/") { InstagramPostsWebView( + colorSchema: viewModel.settingsViewModel.colorSchema, url: url, userAgent: "MacMagazine", shouldUseSidebar: shouldUseSidebar