How to make ScrollView includes WebView in iOS

45 views Asked by At

I want to make a View in ScrollView includes WKWebView like Gmail.
Gmail's body view(webview) to read mail is below.
The WebView can zoom in/out but, top-area(subject, sender...etc) does not zoom.
If I scroll vertically, it will be scrolled with top-area and WebView together.
I am sorry for no good english skill.

enter image description here

struct TopInfoViewModel {
    let subject: String
    let name: String
    let email: String
    let date: String
}

private struct RootView: View {
    let viewModel: TopInfoViewModel
    
    var body: some View {
        VStack(spacing: 0) {
            TopInfoView(viewModel: viewModel)
                .border(.blue)
            
            Color.gray
                .frame(height: 1)
            
            BodyView()
        }
        .padding(.horizontal, 20)
    }
}

struct TopInfoView: View {
    @State private var isOpenedReceiverView: Bool = true
    let viewModel: TopInfoViewModel
    
    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            // 제목
            HStack(spacing: 0) {
                Text(viewModel.subject)
                    .font(.system(size: 18, weight: .bold))
                
                Spacer(minLength: 0)
            }
            
            // Sender
            SenderInfoView(isOpened: $isOpenedReceiverView,
                           name: viewModel.name,
                           email: viewModel.email)
            
            // 날짜, 글자크기 변경, 중요표시
            HStack(spacing: 0) {
                // 날짜
                Text(viewModel.date)
                    .font(.system(size: 13))
            }
        }
        .padding(.vertical, 20)
    }
}

private struct SenderInfoView: View {
    @Binding var isOpened: Bool
    
    let name: String
    let email: String
    
    var body: some View {
        HStack(spacing: 0) {
            Button {
                print("from label touched")
            } label: {
                HStack(spacing: 2) {
                    Text("from")
                        .font(.system(size: 15))
                        .frame(width: 52, alignment: .leading)
                }
            }
            .padding(.vertical, 4)
            
            // name button
            Button {
                print("name touched = \(email)")
            } label: {
                Text(name)
                    .lineLimit(1)
            }
        }
    }
}

struct WebViewWrapper: UIViewRepresentable {
    @State var isLoadedWebView: Bool = false
    
    func makeUIView(context: Context) -> some WKWebView {
        /// 자바스크립트 파일을 불러와 UserContentController에 추가
        /// - Parameters:
        ///   - fileName: 추가할 자바스크립트 파일명. 확장자 제외
        func importJavaScript(fileName: String, userContentController: inout WKUserContentController) {
            if let scriptPath = Bundle.main.path(forResource: fileName, ofType: "js"),
               let scriptSource = try? String(contentsOfFile: scriptPath) {
                let script = WKUserScript(source: scriptSource, injectionTime: .atDocumentStart, forMainFrameOnly: true)
                
                userContentController.addUserScript(script)
            }
        }
        
        let config = WKWebViewConfiguration()
        var userContentController = WKUserContentController()
        
        config.processPool = WKProcessPool()
        config.dataDetectorTypes = .all
        config.websiteDataStore = .nonPersistent()
        userContentController.add(ContentController(), name: "eventNameTest")
        
        // load javascript
        importJavaScript(fileName: "script", userContentController: &userContentController)
        config.userContentController = userContentController
        
        let webView = WKWebView(frame: .zero, configuration: config)
        
        let url = URL(string: "https://www.apple.com")!
        let request = URLRequest(url: url)
        
        webView.isOpaque = false
        webView.backgroundColor = .blue
        webView.navigationDelegate = context.coordinator
        webView.scrollView.decelerationRate = .normal
        webView.scrollView.minimumZoomScale = 1.0
        webView.scrollView.maximumZoomScale = 1.0
        webView.scrollView.isScrollEnabled = false
        webView.backgroundColor = .white
        webView.load(request)
        
        return webView
    }
    
    func updateUIView(_ webView: some WKWebView, context: Context) {
    }
    
    func makeCoordinator() -> WebViewCoordinator {
        WebViewCoordinator(self)
    }
    
    class WebViewCoordinator: NSObject, WKNavigationDelegate {
        var webViewWrapper: WebViewWrapper
        
        init(_ webView: WebViewWrapper) {
            self.webViewWrapper = webView
        }
        
        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            webViewWrapper.isLoadedWebView = true
        }
    }
    
    class ContentController: NSObject, WKScriptMessageHandler {
        func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        }
    }
}

struct BodyView: View {
    var body: some View {
        WebViewWrapper()
            .background(.yellow)
            .border(.red)
    }
}

struct RootView_Preview: PreviewProvider {
    static var previews: some View {
        RootView(viewModel: TopInfoViewModel(subject: "subject1", name: "name1", email: "email1", date: "date1"))
    }
}

enter image description here

1

There are 1 answers

0
MatBuompy On

I assume you wanted the view to be scrollable, I don't understand very well your request, sorry. Anyway, what I did was to wrap the content of the body of your RootView in a GeometryReader and used its size property to give the WebView, named BodyView in this case some witdth and height. Then I also wrapped it in a ScrollView:

private struct RootView: View {
    let viewModel: TopInfoViewModel
    
    var body: some View {
        GeometryReader { proxy in
            let size = proxy.size
            ScrollView {
                VStack(spacing: 0) {
                    TopInfoView(viewModel: viewModel)
                        .border(.blue)
                        .padding(.horizontal)
                    
                    Color.gray
                        .frame(height: 1)
                    
                    BodyView()
                        .padding(.horizontal)
                        .frame(width: size.width, height: size.height)
                }
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

Also, I changed this line to true too:

webView.scrollView.isScrollEnabled = true

I don't exactly know how it is supposed to be set but, if you set it to false the rest View will scroll as if it was an image and you can still zoom in/out in the WebView anyway. Let me know if this is what you meant.