SFSafariViewController vs WKWebView 详细对比

131 阅读7分钟

SFSafariViewController vs WKWebView 详细对比

1. 基本概念对比

特性SFSafariViewControllerWKWebView
类型系统原生Safari浏览器视图控制器自定义WebView组件
引入时间iOS 9.0iOS 8.0
主要用途快速集成Safari浏览体验完全自定义网页浏览功能
内存管理轻量级,系统管理需要手动管理
性能最佳(共享Safari进程)良好(独立进程)

2. 功能特性对比

2.1 完整对比表

功能特性SFSafariViewControllerWKWebView推荐场景
原生Safari体验✅ 完全相同❌ 需要手动实现简单浏览
Cookie共享✅ 自动同步❌ 需要手动配置用户登录
自动填充✅ 支持❌ 不支持表单填写
密码自动填充✅ 支持❌ 不支持安全登录
进度条✅ 内置⚠️ 需要自定义用户体验
导航控件✅ 内置(前进后退)⚠️ 需要自定义导航需求
自动填充✅ 支持❌ 不支持表单填写
书签✅ 支持❌ 需要自定义收藏功能
阅读模式✅ 支持❌ 不支持内容阅读
JavaScript控制❌ 有限✅ 完全控制交互功能
自定义UI❌ 不可修改✅ 完全自定义品牌定制
文件下载✅ 自动处理⚠️ 需要自定义下载管理
新窗口✅ 自动弹出⚠️ 需要自定义多标签页
离线缓存❌ 不支持✅ 可实现离线访问

3. 代码实现对比

3.1 SFSafariViewController 实现


import SafariServices

import UIKit

  


class SafariViewController: UIViewController {

    

    // MARK: - Properties

    private var safariVC: SFSafariViewController?

    private var urlString: String = "https://www.apple.com"

    

    // MARK: - UI Components

    @IBOutlet weak var urlTextField: UITextField!

    @IBOutlet weak var openSafariButton: UIButton!

    

    // MARK: - Lifecycle

    override func viewDidLoad() {

        super.viewDidLoad()

        setupUI()

    }

    

    // MARK: - Setup

    private func setupUI() {

        urlTextField.text = urlString

        openSafariButton.setTitle("在Safari中打开", for: .normal)

    }

    

    // MARK: - Actions

    @IBAction func openSafari(_ sender: UIButton) {

        guard let url = URL(string: urlString) else {

            showError("无效的URL")

            return

        }

        

        // 创建SFSafariViewController

        safariVC = SFSafariViewController(url: url)

        safariVC?.delegate = self

        

        // 配置(可选)

        safariVC?.preferredBarTintColor = .systemBlue

        safariVC?.preferredControlTintColor = .white

        

        // 呈现

        present(safariVC!, animated: true)

    }

    

    @IBAction func urlTextFieldEditingChanged(_ sender: UITextField) {

        guard let text = sender.text, !text.isEmpty else { return }

        urlString = text.hasPrefix("http") ? text : "https://\(text)"

    }

    

    private func showError(_ message: String) {

        let alert = UIAlertController(title: "错误", message: message, preferredStyle: .alert)

        alert.addAction(UIAlertAction(title: "确定", style: .default))

        present(alert, animated: true)

    }

}

  


// MARK: - SFSafariViewControllerDelegate

extension SafariViewController: SFSafariViewControllerDelegate {

    

    func safariViewControllerDidFinish(_ controller: SFSafariViewController) {

        // 用户关闭Safari视图

        controller.dismiss(animated: true)

        print("SafariViewController dismissed")

    }

    

    func safariViewController(_ controller: SFSafariViewController, 

                            didCompleteInitialLoad: Bool) {

        // 页面初始加载完成

        print("Initial load completed")

    }

}

  


// MARK: - 高级配置

class AdvancedSafariViewController: UIViewController {

    

    func openSafariWithConfiguration(url: URL) {

        // 基本配置

        let safariVC = SFSafariViewController(url: url)

        

        // 颜色配置

        safariVC.preferredBarTintColor = UIColor.systemBackground

        safariVC.preferredControlTintColor = UIColor.label

        

        // 自定义关闭按钮

        safariVC.setValue(false, forKey: "usesCompactAppearance")

        

        safariVC.delegate = self

        present(safariVC, animated: true)

    }

    

    // 处理URL输入

    func handleURLInput(_ urlString: String) -> URL? {

        var inputURL = urlString.trimmingCharacters(in: .whitespacesAndNewlines)

        

        // 添加协议

        if !inputURL.hasPrefix("http://") && !inputURL.hasPrefix("https://") {

            inputURL = "https://\(inputURL)"

        }

        

        // 验证URL格式

        guard let url = URL(string: inputURL),

              let scheme = url.scheme,

              ["http", "https"].contains(scheme) else {

            return nil

        }

        

        return url

    }

}

3.2 WKWebView 实现(简化版)


import WebKit

import UIKit

  


class WebViewController: UIViewController {

    

    // MARK: - Properties

    private let webView = WKWebView()

    private var progressView: UIProgressView!

    

    // MARK: - UI Components

    @IBOutlet weak var urlTextField: UITextField!

    @IBOutlet weak var goButton: UIButton!

    

    // MARK: - Lifecycle

    override func viewDidLoad() {

        super.viewDidLoad()

        setupWebView()

        setupProgressView()

        loadInitialURL()

    }

    

    // MARK: - Setup

    private func setupWebView() {

        view.addSubview(webView)

        webView.translatesAutoresizingMaskIntoConstraints = false

        NSLayoutConstraint.activate([

            webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 80),

            webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),

            webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),

            webView.bottomAnchor.constraint(equalTo: view.bottomAnchor)

        ])

        

        webView.navigationDelegate = self

        webView.allowsBackForwardNavigationGestures = true

    }

    

    private func setupProgressView() {

        progressView = UIProgressView(progressViewStyle: .default)

        progressView.translatesAutoresizingMaskIntoConstraints = false

        progressView.progressTintColor = .systemBlue

        view.addSubview(progressView)

        

        NSLayoutConstraint.activate([

            progressView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 60),

            progressView.leadingAnchor.constraint(equalTo: view.leadingAnchor),

            progressView.trailingAnchor.constraint(equalTo: view.trailingAnchor),

            progressView.heightAnchor.constraint(equalToConstant: 2)

        ])

        

        // 监听进度

        webView.addObserver(self, 

                           forKeyPath: #keyPath(WKWebView.estimatedProgress), 

                           options: [.new], 

                           context: nil)

    }

    

    private func loadInitialURL() {

        if let url = URL(string: "https://www.apple.com") {

            webView.load(URLRequest(url: url))

        }

    }

    

    // MARK: - Actions

    @IBAction func goButtonTapped(_ sender: UIButton) {

        guard let urlString = urlTextField.text,

              let url = URL(string: urlString) else {

            showError("无效的URL")

            return

        }

        webView.load(URLRequest(url: url))

    }

    

    @IBAction func backButtonTapped(_ sender: UIButton) {

        if webView.canGoBack {

            webView.goBack()

        }

    }

    

    @IBAction func forwardButtonTapped(_ sender: UIButton) {

        if webView.canGoForward {

            webView.goForward()

        }

    }

    

    @IBAction func refreshButtonTapped(_ sender: UIButton) {

        webView.reload()

    }

    

    override func observeValue(forKeyPath keyPath: String?, 

                              of object: Any?, 

                              change: [NSKeyValueChangeKey : Any]?, 

                              context: UnsafeMutableRawPointer?) {

        if keyPath == #keyPath(WKWebView.estimatedProgress) {

            progressView.progress = Float(webView.estimatedProgress)

            progressView.isHidden = webView.estimatedProgress >= 1.0

        }

    }

    

    deinit {

        webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress))

    }

    

    private func showError(_ message: String) {

        let alert = UIAlertController(title: "错误", message: message, preferredStyle: .alert)

        alert.addAction(UIAlertAction(title: "确定", style: .default))

        present(alert, animated: true)

    }

}

  


// MARK: - WKNavigationDelegate

extension WebViewController: WKNavigationDelegate {

    

    func webView(_ webView: WKWebView, 

                didStartProvisionalNavigation navigation: WKNavigation!) {

        urlTextField.resignFirstResponder()

    }

    

    func webView(_ webView: WKWebView, 

                didFinish navigation: WKNavigation!) {

        if let url = webView.url {

            urlTextField.text = url.absoluteString

        }

        title = webView.title

    }

    

    func webView(_ webView: WKWebView, 

                didFail navigation: WKNavigation!, 

                withError error: Error) {

        showError(error.localizedDescription)

    }

}

4. 使用场景对比

4.1 SFSafariViewController 适用场景


// 场景1: 简单的外部链接浏览

class ArticleViewController: UIViewController {

    @IBAction func openArticle(_ sender: UIButton) {

        guard let url = URL(string: "https://example.com/article") else { return }

        

        let safariVC = SFSafariViewController(url: url)

        present(safariVC, animated: true)

    }

}

  


// 场景2: 社交媒体分享后浏览

class SocialViewController: UIViewController {

    func shareAndOpenInSafari(url: URL) {

        // 分享完成后打开Safari

        let safariVC = SFSafariViewController(url: url)

        safariVC.delegate = self

        present(safariVC, animated: true)

    }

}

  


// 场景3: 需要Safari完整功能的网页

class LoginViewController: UIViewController {

    func openThirdPartyLogin(url: URL) {

        // 第三方登录页面,保持Cookie同步

        let safariVC = SFSafariViewController(url: url)

        present(safariVC, animated: true)

    }

}

4.2 WKWebView 适用场景


// 场景1: 完全自定义的网页应用

class CustomWebAppViewController: UIViewController {

    private let webView = WKWebView()

    

    override func viewDidLoad() {

        super.viewDidLoad()

        setupCustomUI()

        setupJavaScriptBridge()

        loadWebApp()

    }

    

    private func setupCustomUI() {

        // 自定义导航栏

        navigationItem.rightBarButtonItems = [

            UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(reload)),

            UIBarButtonItem(image: UIImage(systemName: "square.and.arrow.up"), 

                           style: .plain, 

                           target: self, 

                           action: #selector(share))

        ]

    }

    

    private func setupJavaScriptBridge() {

        let configuration = WKWebViewConfiguration()

        let userContentController = WKUserContentController()

        userContentController.add(self, name: "nativeBridge")

        configuration.userContentController = userContentController

        webView.configuration = configuration

    }

    

    @objc private func share() {

        // 自定义分享功能

        guard let url = webView.url else { return }

        let activityVC = UIActivityViewController(activityItems: [url], applicationActivities: nil)

        present(activityVC, animated: true)

    }

}

  


// MARK: - WKScriptMessageHandler

extension CustomWebAppViewController: WKScriptMessageHandler {

    func userContentController(_ userContentController: WKUserContentController, 

                              didReceive message: WKScriptMessage) {

        // 处理JavaScript到原生的通信

        if let body = message.body as? [String: Any],

           let method = body["method"] as? String {

            handleNativeMethod(method, parameters: body["params"] as? [String: Any])

        }

    }

    

    private func handleNativeMethod(_ method: String, parameters: [String: Any]?) {

        switch method {

        case "scanQRCode":

            openCameraScanner()

        case "getLocation":

            requestLocationPermission()

        case "shareContent":

            shareWebContent(parameters)

        default:

            break

        }

    }

}

  


// 场景2: 企业内部网页门户

class EnterprisePortalViewController: UIViewController {

    private let webView = WKWebView()

    

    func setupEnterpriseWebView() {

        // 自定义安全策略

        let configuration = WKWebViewConfiguration()

        configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent() // 不持久化数据

        

        // 注入企业安全脚本

        let securityScript = WKUserScript(

            source: enterpriseSecurityJavaScript(),

            injectionTime: .atDocumentStart,

            forMainFrameOnly: true

        )

        configuration.userContentController.addUserScript(securityScript)

        

        webView.configuration = configuration

        webView.navigationDelegate = self

    }

    

    private func enterpriseSecurityJavaScript() -> String {

        """

        // 企业安全策略

        (function() {

            // 禁用右键菜单

            document.addEventListener('contextmenu', function(e) {

                e.preventDefault();

            });

            

            // 禁用开发者工具

            document.addEventListener('keydown', function(e) {

                if (e.key === 'F12' || (e.ctrlKey && e.shiftKey && e.key === 'I')) {

                    e.preventDefault();

                }

            });

            

            // 内容安全检查

            window.addEventListener('beforeunload', function(e) {

                // 敏感页面退出确认

                if (isSensitivePage()) {

                    e.returnValue = '确定要离开此页面吗?';

                }

            });

        })();

        """

    }

}

  


// 场景3: 离线内容和混合应用

class HybridAppViewController: UIViewController {

    private let webView = WKWebView()

    private let offlineManager = OfflineContentManager.shared

    

    func loadHybridContent(urlString: String) {

        guard let url = URL(string: urlString) else { return }

        

        // 首先尝试加载离线内容

        offlineManager.loadCachedPage(for: url) { [weak self] html in

            if let html = html {

                // 加载离线内容

                self?.webView.loadHTMLString(html, baseURL: url)

            } else {

                // 加载在线内容并缓存

                self?.webView.load(URLRequest(url: url))

            }

        }

    }

}

5. 性能和内存对比

5.1 内存使用对比


// 内存监控工具

class MemoryMonitor {

    static func logMemoryUsage(for webView: WKWebView) {

        // 获取当前内存使用

        var info = mach_task_basic_info()

        var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size)/4

        let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) {

            $0.withMemoryRebound(to: integer_t.self, capacity: 1) {

                task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), 

                         $0, &count)

            }

        }

        

        if kerr == KERN_SUCCESS {

            let usedBytes = info.resident_size

            let usedMB = Double(usedBytes) / 1024 / 1024

            print("WebView 内存使用: \(String(format: "%.1f", usedMB)) MB")

        }

    }

}

  


// SFSafariViewController 内存管理(自动)

class SafariMemoryManager {

    static func optimizeSafariMemory() {

        // SFSafariViewController 自动优化

        // 无需手动管理

        print("SFSafariViewController: 自动内存管理")

    }

}

  


// WKWebView 内存管理(手动)

class WebViewMemoryManager {

    static func cleanupWebView(_ webView: WKWebView) {

        // 清理缓存

        WKWebsiteDataStore.default().removeData(

            ofTypes: WKWebsiteDataTypeAll,

            modifiedSince: Date.distantPast

        ) { completed in

            print("WebView 缓存清理完成")

        }

        

        // 停止所有加载

        webView.stopLoading()

        

        // 清理JavaScript上下文

        webView.configuration.userContentController.removeAllUserScripts()

        webView.configuration.userContentController.removeScriptMessageHandler(forName: "nativeBridge")

    }

}

6. 选择指南

6.1 快速决策表

需求推荐选择原因
✅ 简单链接浏览SFSafariViewController开发快,用户体验好
✅ 需要Safari完整功能SFSafariViewControllerCookie同步,自动填充
✅ 完全自定义UIWKWebView灵活控制
✅ JavaScript交互WKWebView完整的JS桥接
✅ 企业安全需求WKWebView自定义安全策略
✅ 离线内容WKWebView缓存控制
✅ 混合应用WKWebView原生+Web 集成
✅ 轻量级集成SFSafariViewController最小代码量

6.2 混合使用策略


class SmartWebController: UIViewController {

    

    enum WebViewType {

        case safari

        case webkit

    }

    

    private func chooseWebViewType(for url: URL, requirements: WebRequirements) -> WebViewType {

        // 智能选择策略

        if requirements.needsCustomUI || 

           requirements.needsJavaScriptBridge ||

           requirements.needsOfflineSupport {

            return .webkit

        }

        

        if requirements.needsCookieSync ||

           requirements.needsAutoFill ||

           requirements.isSimpleBrowse {

            return .safari

        }

        

        return .safari // 默认选择

    }

    

    struct WebRequirements {

        let needsCustomUI: Bool

        let needsJavaScriptBridge: Bool

        let needsOfflineSupport: Bool

        let needsCookieSync: Bool

        let needsAutoFill: Bool

        let isSimpleBrowse: Bool

    }

    

    func openWebView(url: URL, requirements: WebRequirements) {

        let type = chooseWebViewType(for: url, requirements: requirements)

        

        switch type {

        case .safari:

            openInSafari(url: url)

        case .webkit:

            openInWebView(url: url)

        }

    }

    

    private func openInSafari(url: URL) {

        let safariVC = SFSafariViewController(url: url)

        present(safariVC, animated: true)

    }

    

    private func openInWebView(url: URL) {

        let webVC = WebViewController()

        webVC.loadURL(url.absoluteString)

        navigationController?.pushViewController(webVC, animated: true)

    }

}

  


// 使用示例

class ContentViewController: UIViewController {

    

    @IBAction func openNewsArticle(_ sender: UIButton) {

        guard let url = URL(string: "https://news.example.com/article") else { return }

        

        let requirements = SmartWebController.WebRequirements(

            needsCustomUI: false,

            needsJavaScriptBridge: false,

            needsOfflineSupport: false,

            needsCookieSync: true,

            needsAutoFill: true,

            isSimpleBrowse: true

        )

        

        let smartController = SmartWebController()

        smartController.openWebView(url: url, requirements: requirements)

    }

    

    @IBAction func openWebApp(_ sender: UIButton) {

        guard let url = URL(string: "https://webapp.company.com") else { return }

        

        let requirements = SmartWebController.WebRequirements(

            needsCustomUI: true,

            needsJavaScriptBridge: true,

            needsOfflineSupport: true,

            needsCookieSync: false,

            needsAutoFill: false,

            isSimpleBrowse: false

        )

        

        let smartController = SmartWebController()

        smartController.openWebView(url: url, requirements: requirements)

    }

}

7. 总结建议

7.1 最佳实践

  1. 简单浏览需求 → SFSafariViewController

   - 外部链接、新闻文章、社交分享

   - 需要Safari完整功能的用户体验

  1. 复杂交互需求 → WKWebView

   - 自定义UI、JavaScript桥接、企业应用

   - 需要完全控制的场景

  1. 混合策略

   - 简单内容用SFSafariViewController

   - 核心功能用WKWebView

   - 智能切换机制

7.2 性能优化建议

SFSafariViewController:

  • 最小代码,自动优化

  • 无需手动内存管理

WKWebView:


// 内存优化最佳实践

class OptimizedWebView: WKWebView {

    

    override init(frame: CGRect, configuration: WKWebViewConfiguration?) {

        let config = configuration ?? WKWebViewConfiguration()

        // 优化配置

        config.processPool = WKProcessPool() // 共享进程池

        config.websiteDataStore = WKWebsiteDataStore.default() // 共享数据存储

        

        super.init(frame: frame, configuration: config)

        

        // 限制内存使用

        allowsLinkPreview = false

        isOpaque = false

    }

    

    required init?(coder: NSCoder) {

        fatalError("init(coder:) has not been implemented")

    }

    

    func optimizeForLowMemory() {

        // 清理不必要的资源

        stopLoading()

        configuration.userContentController.removeAllUserScripts()

    }

}