iOS Swift与Javascript的热恋影集

移动开发 来源:Castie1 63℃ 0评论

喵神100Tips的解读连载已经到了第十篇了, 是要好好敲一个Demo来巩固一下自己Swift的学习情况, 说来也巧, 前两天领导来问我能不能帮他做一个需求, 要求web页面调用app原生相册, 选择照片后将图片加载到web页面上. (同学们先想想怎样才能够实现这个需求), 这正是一个锻炼Swift3的机会, 绝对不能错过, 虽然一口答应, 但实际操作中还是有一些技术难点存在的, 在此与你分享.

要做这个功能, 虽然对我来说还是Objective-C比较熟悉, 各项技术黑魔法都手到擒来, 但是Swift毕竟是未来的趋势, 到Swift3后API也趋于稳定, 因此我就选择了Swift作为app端开发语言. web端当仁不让的就选择Javascript了, 领导一开始要求我使用HTML5的Canvas来实现, 可杀鸡焉用牛刀, 我就简单的使用img标签代替了, 其实Canvas与Quartz2D的API极为相似, 有经验的同学很好上手!

我们先来分析一下这个需求, 更好的拆解需求能够设计出理想的代码, 首先, 我们想到了需要一个WebView, 然后实现代理进行交互, 但是一般来说我们与H5交互都是传字符串, 如何传输图片数据呢? (在此声明, 此需求不涉及请求及服务端)

带着这个问题, 我们先把工程创建起来, 毕竟良好的开端是成功的一半!

fileprivate lazy var webView: UIWebView = {[weak self] in
     let webView = UIWebView()
     webView.delegate = self       
     webView.frame = CGRect(x: 0,
                            y: 0,
                            width:  kScreenW,
                            height: kScreenH)
     return webView
}()

我们先通过懒加载创建webView, 与Objective-C重写get方法不同的是, Swift中自带lazy关键字, 可以轻松实现懒加载, 上面是通过闭包形式的懒加载方式, 所以在闭包中引用self的话会造成循环引用, 所以我们需要加上[weak self] in, fileprivate关键字是Swift3更新的, 意思是本文件中似私有.

extension ViewController {

    fileprivate func loadWebView() {
        guard let file = Bundle.main.path(forResource: "index", ofType: "html") else { return }
        guard let htmlString = try? String(contentsOfFile: file, encoding: .utf8) else { return }
        let baseURL = URL(fileURLWithPath: Bundle.main.bundlePath)
        webView.loadHTMLString(htmlString, baseURL: baseURL)
    }
}

创建完webView我们就需要加载本地的HTML文件了, 加载方式还是和Objective-C的一样, 但需要注意的是我把这个scope放在了一个extension中, extension是Swift中本人最为喜欢的功能了, 对比Objective-C的Catagroy来说, 它的装饰模式的灵活性真是强大太多了.(不理解装饰模式的同学可以看看设计模式方面的书籍) guard也是Swift中我比较喜欢的功能, 可选绑定校验能够使代码逻辑更为清晰, 对我这种对代码美感有强迫症的人来说, 真是太合适不过了. 第三个注意点事try?, 在Objective-C的时代我们对异常的处理其实非常的少, 但在Swift中却极为常见, 可能是对安全方面的考量吧, 有兴趣的同学可以看看我之前分享的喵神的书中就有这一段分析.

override func viewDidLoad() {
      super.viewDidLoad()
      setupWebView()
      loadWebView()    
}

extension ViewController {

    fileprivate func setupWebView() {
        view.addSubview(webView)
    }
}

我们把webView添加到控制器的view上并进行读取HTML文件.

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>PhotoGallery</title>

<style>

p {
    margin: 0;
    padding: 0;
    text-align: center;
    font-size: 18px;
}

</style>
</head>

<body>

![](placeholder.png)
<p> # click here to select a photo # </p>

<script type="text/javascript">

</script>
</body>

</html>

接下来我们先创建一个非常简单的HTML文件, 里面只包含了一张图片和一段文字, 这里我图个方便将CSS和JS都集成在HTML文件中了, 一般做项目建议分开引用. script标签按照规范放置在body标签的最后, 这是web项目最基础的优化.

function prepareParagraphEvent() {
    if (!document.getElementsByTagName) return false;
    var ps = document.getElementsByTagName("p");
    for (var i = 0; i < ps.length; i++) {
        ps[i].onclick = function() {
            window.location.href = "ios://openPhotoGallery";
        }
    }
}

我们通过操作DOM的方式来给p标签绑定一个点击事件, 当然你也可以用jQuery这种JS库, 我这里就用原生的API进行实现了, 第一个判断的的意义是要让网页做到平稳退化, 当然现在大部分浏览器都支持的很好, 但我们也要照顾到那些不支持JS的浏览器不是吗? 这里有一个技术点: ios://openPhotoGallery 这个字符串其实就是跳转的关键, 如果在工作中就需要app端和web端制定规则, 因为app端需要根据这段字符串来进行解析.

function addLoadEvent(func) {
    var oldonload = window.onload;
    if (typeof window.onload != 'function') {
        window.onload = func;
    } else {
        window.onload = function() {
            oldonload();
            func();
        }
    }
}

addLoadEvent(prepareParagraphEvent);

我们默认加载此prepareParagraphEvent函数, window.onload可以简单的理解为viewDidLoad.

extension ViewController: UIWebViewDelegate {

    func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool {
        guard let href = request.url?.absoluteString else { return false }
        if href.hasPrefix("ios") {
            guard let method = href.components(separatedBy: "://").last else { return false }
            let selector = Selector.init(method)
            if self.responds(to: selector) {
                self.perform(selector)
            }
            return false
        }
        return true
    }
}

当window.location.href触发的时候, 会调用UIWebViewDelegate的上述代理方法. 我们根据刚才所制定的规则进行解析, 我这里是使用类似协议头的方式, 这样看起来更加的正式, 当然你也可以制定你想制定的任何一种方式. 我们根据协议头后的方法名字符串通过桥接Objective-C的runtime来动态执行方法.

extension ViewController {

    @objc fileprivate func openPhotoGallery() {
        if UIImagePickerController.isSourceTypeAvailable(.photoLibrary){
            let picker = UIImagePickerController()
            picker.delegate = self
            picker.sourceType = .photoLibrary
            picker.allowsEditing = true
            self.present(picker, animated: true)
        } else {
            print("access photo gallery error")
        }
    }
}

当然我们也要准备相应的方法, @objc 关键字 就是进行与Objective-C的桥接, 方法内部的功能是,打开系统图片库, 这代码我就不分析了, 因为实在是没有什么可说的.. 到这一步, 我们就已经实现了一半了, 通过点击web中的标签打开了app的系统的照片库.当然就像伟大航线的新世界一样, 好戏在后头.

extension ViewController: UIImagePickerControllerDelegate,UINavigationControllerDelegate {

    func imagePickerController(_ picker: UIImagePickerController,didFinishPickingMediaWithInfo info: [String : Any]) {
        let image = info[UIImagePickerControllerOriginalImage] as? UIImage
        guard let Document = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).last
            else { return }
        let path = "\(Document)/\("image\(arc4random()%100).png")"
        if FileManager.default.fileExists(atPath: path) {
            try! FileManager.default.removeItem(atPath: path)
        }
        try! UIImagePNGRepresentation(image!)?.write(to: URL(fileURLWithPath: path))
        picker.dismiss(animated: true) {
            self.webView.stringByEvaluatingJavaScript(from: "loadImageWithPath('\(path)');")
        }
    }
}

这段代码可谓是这个Demo中最核心的一段了, 这个是UIImagePickerController的代理方法回调, 简单来说就是拿到选择的照片, 我实现的思路是将图片写入沙盒并将沙盒路径传递给Javascript. (在做这个的过程中了解到了一个之前忽视掉的细节, 就是在iOS8开始, 每次打开沙盒的路径都是不同的) 传递的核心代码就是 stringByEvaluatingJavaScript(from: "loadImageWithPath('(path)');, 就是调用JS的函数.

function loadImageWithPath(src) {
    if (!document.getElementsByTagName) return false;
    var imgs = document.getElementsByTagName("img");
    for (var i = 0; i < imgs.length; i++) {
        imgs[i].src = src;
    }
}

通过调用函数, 更改图片路径, 我们的功能也终于大功告成啦!!! 来看一下实现的效果.


其实还有一个bug并没有很好的解决, let path = "(Document)/("image(arc4random()%100).png")", 我使用了随机数生成的图片名来抑制bug的产生, 但这并不是合适的做法, 如果不生成随机数的话, 切换第一张图片后, 第二张图片将不会切换, 但沙盒中的图片数据已经发生更改, 为了解决这个bug, 还请大神们伸出援助之手吧!

github 下载地址!!!


点击下方链接跳转!!
具体源码 请到github上进行下载! 喜欢的朋友送下小星星哟!!

关闭

IT问道推荐

银行贷款频频被拒?
“Dr信用牛牛”让你远离信用污点 国内首家信用健康管理平台免费为你提供信用修复方案