iOS组件化初探

iOS组件化初探_第1张图片

 iOS组件化初探_第2张图片 

本文字数:17361

计划阅读时间:44分钟

iOS组件化初探

1.背景

  就目前而言,iOS 项目的组件化在业内已经有比较成熟的方案了。虽然各个公司都有自己的组件化方案,但这些方案的具体实现方式也都大同小异。截止到本次组件化改造之前,我所在的 iOS 开发团队尚未对项目进行组件化改造,单个模块在多个项目中的复用仍使用手动复制迁移的方式。现有的一些功能模块也基本是使用 OC 语言开发的。如下图所示,假如现有项目使用了功能模块A,而功能模块A又依赖功能模块B,此时有新项目也要使用功能模块A,就需要将功能模块A功能模块B的源码全部手动复制到新项目的工程中。

iOS组件化初探_第3张图片


这样做有以下几个问题:

  1. 不利于模块的统一管理

如果有N个项目依赖同一个模块,就会有N个该模块的实体副本分散的存在于N个项目工程中。如果该模块有内容更新,就需要对全部的这N个模块副本进行更新,不仅操作起来十分麻烦,也很容易产生遗漏。

  1. 不利于模块的独立调试

在这种管理方式下,模块依附于其宿主工程而存在,要想调试模块的功能,需要打开宿主工程并在其上进行调试。大于大型项目而言,主工程的编译和运行往往需要较长时间。

  1. 难以维护、不可持续

业务逻辑和功能模块之间、功能模块和功能模块之间没有严格的界限,代码耦合程度完全依靠编码人员自身素质决定。随着版本的不断迭代,加之开发人员的更替,项目代码将快速劣化变得难以维护。

以上只是列举了几点这种管理代码复用方式的不足之处,此外还有模块版本管理、自动化等其他问题,就不一一展开说明了。

鉴于 Swift 语言的高效率和安全性,业内对其的应用也越来越广泛,团队内的一些新 App 以及功能模块逐渐开始使用 Swift 开发,这些老旧的功能模块也将逐渐被取代。此外,随着部门内开发和维护的 App 越来越多,组件化作为一项基础设施急需得到落实,不仅方便开发人员进行项目管理,也可以方便的进行组件在多个项目中的复用,提升开发效率。

目前焦点 iOS 组件化已初见成效,本篇文章将通过其中一个组件的改造实践作为案例,给大家介绍一下进行组件化改造的基本流程。

2.基本原理

开始之前我先为大家说明一下我们组件化的基本原理。我们采取的组件化方案是基于cocoapods实现的,也是业内使用比较普遍的一种方案。最终效果是,将我们希望实施组件化的模块从主工程中解耦出来成为独立组件,并制作成本地 pod 库,再通过cocoapods集成到项目中,被独立出来的组件使用单独的 git 仓库管理。类似于使用 cocoapods 集成第三方库一样,只不过我们的组件库是一个本地的 pod 库。

因此,要想制作一个组件库,首先要知道如何制作本地的 pod 库。

pod 库主要由三部分组成:源码文件、资源文件和podespec文件。源码文件和资源文件暂且不说,每个 pod 库都要有一个名字为库名称.podspec的文件,官方称其为specificationcocoapods官方对该文件的解释为:

A specification describes a version of Pod library. It includes details about where the source should be fetched from, what files to use, the build settings to apply, and other general metadata such as its name, version, and description.

大意为:该文件描述了关于 pod 库的所有配置。包括从何处获取源代码、使用哪些文件、应用构建设置以及其他一些元数据(如名称、版本和描述)的详细信息。

可以通过三种方式来创建podspec文件:

1. pod lib create xxxx

xxxx表示创建的文件名,这种方式比较适合从零开始开发一个组件,因为它会自动帮我们生成许多模版。在终端执行以上命令,命令执行过程中,会询问几个问题,根据实际情况和需要回答即可。这里以QRCodeReader为例:

lanfudong@MacBook-Pro ~ % pod lib create QRCodeReader 
Cloning `https://github.com/CocoaPods/pod-template.git` into `QRCodeReader`.
Configuring QRCodeReader template.
security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain.

------------------------------

To get you started we need to ask a few questions, this should only take a minute.

If this is your first time we recommend running through with the guide: 
 - https://guides.cocoapods.org/making/using-pod-lib-create.html
 ( hold cmd and double click links to open in a browser. )

What platform do you want to use?? [ iOS / macOS ]
 > iOS

What language do you want to use?? [ Swift / ObjC ]
 > Swift

Would you like to include a demo application with your library? [ Yes / No ]
 > Yes

Which testing frameworks will you use? [ Quick / None ]
 > None

Would you like to do view based testing? [ Yes / No ]
 > No

执行完成后,会在当前目录下创建一个以QRCodeReader命名的文件夹,并在文件夹内自动生成了QRCodeReader.podspec文件和若干模板文件,如下图所示:

iOS组件化初探_第4张图片

其中 QRCodeReader 文件夹中存放的就是该组件的源码和资源文件。 Example 文件夹下是该命令帮我们创建的一个示例工程, Example 工程默认已经集成了新创建的组件,我们可以直接在 Example 工程的基础上进行编码。 _Pods.xcodeproj 文件是 Example 文件夹下的 QRCodeReader.xcodeproj 文件的替身。

打开Example工程,先来看下Podfile文件:

use_frameworks!

platform :ios, '10.0'

target 'QRCodeReader_Example' do
  pod 'QRCodeReader', :path => '../'

  target 'QRCodeReader_Tests' do
    inherit! :search_paths

  end
end

主要看pod 'QRCodeReader', :path => '../'这行代码,表示通过指定路径的方式集成QRCodeReader组件。这里的QRCodeReader组件目录位于Podfile文件的前一级目录下。

再来看QRCodeReader.podspec文件,内部已经自动填充了代码模版,关于每行代码的具体含义,我们后续再着重介绍。

podspec其实是一个ruby语言的脚本文件,里面的文本内容也都是ruby代码。这里不需要我们懂得ruby语言,只要能读懂其大体含义即可。

#
# Be sure to run `pod lib lint QRCodeReader.podspec' to ensure this is a
# valid spec before submitting.
#
# Any lines starting with a # are optional, but their use is encouraged
# To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html
#

Pod::Spec.new do |s|
  s.name             = 'QRCodeReader'
  s.version          = '0.1.0'
  s.summary          = 'A short description of QRCodeReader.'

# This description is used to generate tags and improve search results.
#   * Think: What does it do? Why did you write it? What is the focus?
#   * Try to keep it short, snappy and to the point.
#   * Write the description between the DESC delimiters below.
#   * Finally, don't worry about the indent, CocoaPods strips it!

  s.description      = <<-DESC
TODO: Add long description of the pod here.
                       DESC

  s.homepage         = 'https://github.com/lanfudong/QRCodeReader'
  # s.screenshots     = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  s.author           = { 'lanfudong' => '[email protected]' }
  s.source           = { :git => 'https://github.com/lanfudong/QRCodeReader.git', :tag => s.version.to_s }
  # s.social_media_url = 'https://twitter.com/'

  s.ios.deployment_target = '10.0'

  s.source_files = 'QRCodeReader/Classes/**/*'
  
  # s.resource_bundles = {
  #   'QRCodeReader' => ['QRCodeReader/Assets/*.png']
  # }

  # s.public_header_files = 'Pod/Classes/**/*.h'
  # s.frameworks = 'UIKit', 'MapKit'
  # s.dependency 'AFNetworking', '~> 2.3'
end

2. pod spec create xxxx

xxxx就是要创建的podspec文件名,这种方式比较适合对现有模块进行组件化改造。执行此命令后,仅会在当前文件夹中创建一个xxxx.podspec文件,不会生成任何模版文件。此处仍以QRCodeReader为例:

lanfudong@MacBook-Pro ~ % pod spec create QRCodeReader

Specification created at QRCodeReader.podspec

打开文件后可以看见里面同样也预填充了代码模板和注释。

#
#  Be sure to run `pod spec lint QRCodeReader.podspec' to ensure this is a
#  valid spec and to remove all comments including this before submitting the spec.
#
#  To learn more about Podspec attributes see https://guides.cocoapods.org/syntax/podspec.html
#  To see working Podspecs in the CocoaPods repo see https://github.com/CocoaPods/Specs/
#

Pod::Spec.new do |spec|

  # ―――  Spec Metadata  ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
  #
  #  These will help people to find your library, and whilst it
  #  can feel like a chore to fill in it's definitely to your advantage. The
  #  summary should be tweet-length, and the description more in depth.
  #

  spec.name         = "QRCodeReader"
  spec.version      = "0.0.1"
  spec.summary      = "A short description of QRCodeReader."

  # This description is used to generate tags and improve search results.
  #   * Think: What does it do? Why did you write it? What is the focus?
  #   * Try to keep it short, snappy and to the point.
  #   * Write the description between the DESC delimiters below.
  #   * Finally, don't worry about the indent, CocoaPods strips it!
  spec.description  = <<-DESC
                   DESC

  spec.homepage     = "http://EXAMPLE/QRCodeReader"
  # spec.screenshots  = "www.example.com/screenshots_1.gif", "www.example.com/screenshots_2.gif"


  # ―――  Spec License  ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
  #
  #  Licensing your code is important. See https://choosealicense.com for more info.
  #  CocoaPods will detect a license file if there is a named LICENSE*
  #  Popular ones are 'MIT', 'BSD' and 'Apache License, Version 2.0'.
  #

  spec.license      = "MIT (example)"
  # spec.license      = { :type => "MIT", :file => "FILE_LICENSE" }


  # ――― Author Metadata  ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
  #
  #  Specify the authors of the library, with email addresses. Email addresses
  #  of the authors are extracted from the SCM log. E.g. $ git log. CocoaPods also
  #  accepts just a name if you'd rather not provide an email address.
  #
  #  Specify a social_media_url where others can refer to, for example a twitter
  #  profile URL.
  #

  spec.author             = { "lanfudong" => "[email protected]" }
  # Or just: spec.author    = "lanfudong"
  # spec.authors            = { "lanfudong" => "[email protected]" }
  # spec.social_media_url   = "https://twitter.com/lanfudong"

  # ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
  #
  #  If this Pod runs only on iOS or OS X, then specify the platform and
  #  the deployment target. You can optionally include the target after the platform.
  #

  # spec.platform     = :ios
  # spec.platform     = :ios, "5.0"

  #  When using multiple platforms
  # spec.ios.deployment_target = "5.0"
  # spec.osx.deployment_target = "10.7"
  # spec.watchos.deployment_target = "2.0"
  # spec.tvos.deployment_target = "9.0"


  # ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
  #
  #  Specify the location from where the source should be retrieved.
  #  Supports git, hg, bzr, svn and HTTP.
  #

  spec.source       = { :git => "http://EXAMPLE/QRCodeReader.git", :tag => "#{spec.version}" }


  # ――― Source Code ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
  #
  #  CocoaPods is smart about how it includes source code. For source files
  #  giving a folder will include any swift, h, m, mm, c & cpp files.
  #  For header files it will include any header in the folder.
  #  Not including the public_header_files will make all headers public.
  #

  spec.source_files  = "Classes", "Classes/**/*.{h,m}"
  spec.exclude_files = "Classes/Exclude"

  # spec.public_header_files = "Classes/**/*.h"


  # ――― Resources ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
  #
  #  A list of resources included with the Pod. These are copied into the
  #  target bundle with a build phase script. Anything else will be cleaned.
  #  You can preserve files from being cleaned, please don't preserve
  #  non-essential files like tests, examples and documentation.
  #

  # spec.resource  = "icon.png"
  # spec.resources = "Resources/*.png"

  # spec.preserve_paths = "FilesToSave", "MoreFilesToSave"


  # ――― Project Linking ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
  #
  #  Link your library with frameworks, or libraries. Libraries do not include
  #  the lib prefix of their name.
  #

  # spec.framework  = "SomeFramework"
  # spec.frameworks = "SomeFramework", "AnotherFramework"

  # spec.library   = "iconv"
  # spec.libraries = "iconv", "xml2"


  # ――― Project Settings ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
  #
  #  If your library depends on compiler flags you can set them in the xcconfig hash
  #  where they will only apply to your library. If you depend on other Podspecs
  #  you can include multiple dependencies to ensure it works.

  # spec.requires_arc = true

  # spec.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" }
  # spec.dependency "JSONKit", "~> 1.4"

end

本篇文章不会逐一对里面的内容进行讲解,在接下来的篇章中会结合具体案例来讲解其中几个比较重要的部分。其余部分读者可参照官方文档进行阅读

https://guides.cocoapods.org/syntax/podspec.html#specification

3. 手动创建

如果你对podspec文件已经非常熟悉了,可以直接手动创建,或者复制一份现有的并在其基础上进行修改。

总结:如果你准备从零开始开发一个新的组件,那么适合使用第一种方式来初始化组件化工程,它会帮你自动生成一些列的模版文件和代码,可以直接在Example工程上进行组件的开发和调试;如果你是想对现有的模块进行组件化改造,已经存在了源码和资源等文件,那么适合使用第二或第三种方式来初始化组件化工程。

3.创建第一个组件库

本文主要讲对现有项目进行组件化改造,这里仍然以QRCodeReader为例,继续为大家讲解。

还记得上面提到的pod库三大组成部分吗?源码 + 资源 + podspec 文件,接下来要搞定源码文件。QRCodeReader的源码文件目前还在项目的主工程里面,若想将其独立出来,我们需要将其进行适当的修改,分为两个方面:

iOS组件化初探_第5张图片


其一:解耦合。对于像QRCodeReader这种比较简单的组件,几乎没什么耦合,可直接将其从主工程中移出来。文件被移出后自然会出现“某某类或某某方法找不到“之类的错误,先不要着急,等完成组件化配置后再来解决此类编译问题;若是比较庞大的组件,耦合性较高,最好是先理清依赖关系,完成解耦。

对于复杂系统,解耦合的主要思路是使用中间件桥接,即各个组件间不直接相互访问,而是都通过一个中间者来实现,这就要求每个组件将自身所具有的能力注册到中间件中,是六大设计原则之一的依赖倒转原则的具体实现。如下图所示,组价A组件B彼此独立,组件A通过中间件来访问组件B。这里仅提供一种思路,感兴趣的读者可以自行搜索相关资料阅读,本篇不做过多介绍。

iOS组件化初探_第6张图片

其二:修改访问级别(Access Levels)。对于 Swift 来说,我们声明的类和方法等的访问级别默认都是 internal 的,即只能供同一 Module 内的文件访问。进行组件化改造后,我们制作的组件会成为一个独立的 Module,因此需要将组件内暴露给外部的类、方法、属性等设置为 publicopen ,才能被其他 Module 访问。

Access Levels

open:最高访问级别,只能修饰类和类成员,允许任何地方的代码访问,允许被Module外的代码继承和重写

public:允许任何地方的代码访问,但不允许被Module外的代码继承和重写

internal:默认访问级别,允许Module内任何地方的代码访问,Module外无法访问

fileprivate:仅允许同一个源文件内的代码访问

private:最低访问级别,仅允许同一个实体内的代码访问

完成这两步后就可以把源码文件移动到组件化目录下了,大家可根据需要把”组件化目录“放到磁盘上的任意位置,这里我将其放到和主工程同级的目录下:

这里的组件化目录指的是磁盘上创建的一个文件夹,不是 Xcode 项目目录,也不要把这个目录放到主工程的 git 仓库下,因为后续会把该目录提交到一个单独的远程仓库管理。

前面已经完成了podspec文件的创建,现在开始编辑其内容,资源文件留到最后处理。需要注意的是,podspec文件必须与组件库同名且放在其根目录下。

iOS组件化初探_第7张图片

我这里使用第三种方法手动创建的 QRCodeReader.podspec文件,文件内容我已经编辑好,一起来看下:
Pod::Spec.new do |spec|

    spec.name         = "QRCodeReader"
    spec.version      = "0.0.1"
    spec.summary      = "扫码组件"
    spec.description  = <<-DESC
                     扫码组件
                      DESC
    spec.license      = { :type => "MIT" }
    spec.author       = { "用户名" => "你的邮箱" }
    spec.homepage     = "项目主页"
    spec.source       = { :git => "项目git仓库", :tag => "#{spec.version}" }
    spec.platform     = :ios, "11.0"

    spec.swift_versions = ['5.0', '5.1', '5.2', '5.3']

    spec.source_files    = "*.{swift,m,mm,c,cpp,h}"
    # spec.resources     = "Resources/**/*.{xcassets,json,plist}"
    spec.resource_bundle = { "QRCodeReader" => "Resources/**/*.{xcassets,json,plist}" }

    spec.xcconfig = {
        'DEFINES_MODULE' => 'YES'
    }

    spec.frameworks = "AudioToolbox", "AVFoundation"
    
end

Pod::Spec.new do |spec|end是固定语法,用来声明一个新的specification,分别表示声明的开始和结束,中间部分设置specification的各项属性。其中多数属性可以顾名知义,这里选几个比较重要的说明一下。

spec.platform:指定应用的平台和系统版本。案例中的含义是应用于 iOS 系统 11.0 及以上版本;

spec.swift_versions:指定使用的 Swift 语言版本,值是一个数组;

spec.source_files:指定源代码文件的路径和文件名,等号右边可使用通配符的方式指定。案例中的含义是:源码文件包含当前目录下的所有文件类型为 swift、m、mm、c、cpp、h 的文件;

spec.resources:指定资源文件的路径和文件名,等号右边可使用通配符的方式指定。案例中的含义是:资源文件包含当前目录下的Resources目录下的所有文件类型为 xcassets、json、plist 的文件。后续我们提取资源文件的时候会将其放到Resources目录下;

spec.xcconfig:对 pod 库进行配置,配置的内容最终会反应到 pod 库的Build Settings上。案例中的含义是:配置 pod 库的Defines Module为 YES。除了DEFINES_MODULE外,还有很多内容可以配置,这里不一一介绍;

spec.frameworks:指定需要链接的系统动态库。

此外还有些案例中没有提及的但也比较常用的属性有:

spec.libraries:用来指定链接的系统静态库;

spec.dependency:用来指定该组件所依赖的其他组件;

spec.vendored_frameworks:用来指定该组件依赖的第三方framework。

上面例举的都是常用属性,Cocoapods提供了非常多的属性供我们使用,读者可参照官方文档进行阅读:https://guides.cocoapods.org/syntax/podspec.html#specification

4.集成组件到项目中

修改Podfile文件,添加如下代码:

pod 'QRCodeReader', :path => '../QRCodeReader'

:path后面的路径即为组件库相对于Podfile文件的位置,还记得前面我们把组件库放到和主工程同级目录了吗?这里的含义是”集成QRCodeReader组件,组件位于Podfile文件上级目录中的QRCodeReader目录“。

之后我们在主工程中执行pod install命令,看见如下输出,说明集成成功了。

iOS组件化初探_第8张图片

打开项目后,可以看见在左侧导航区 Pods 工程下多了一个 Development Pods 目录,我们集成的 QRCodeReader 组件就在这里面了。

iOS组件化初探_第9张图片

编译项目后可能会出现“Cannot find 'xxxx' in scope”等找不到类或方法的问题,这是因为这些类和方法已经被我们封装成为独立 Module,主工程若想访问其中的内容需要先引入该 Module。

在使用了扫码组件的文件中添加代码“import QRCodeReader”,编译通过~

需要注意的是,不同于在Podfile中使用pod 'xxx', "~> 1.0.0"的方式集成第三方库,通过指定组件库本地路径的方式集成,组件库中的文件不会被拷贝到主工程中,主工程中的组件库只是一个”引用“,类似于面向对象编程中的”对象指针“的概念。

iOS组件化初探_第10张图片

5.资源文件的管理

QRCodeReader组件里包含一个扫码界面,会用到一些图标资源。若把这些图标保留在主工程的Assets中,虽然仍然可以访问到,可一旦我们将QRCodeReader组件集成到其他项目中,这些图标资源就不能被自动集成过去,需要我们手动迁移。这样做非常麻烦不说,也和容易发生遗漏。

因此,最好的方式是将这些图标资源转移到对应的组件库中,并由组件库统一管理。

QRCodeReader组件里用到的资源文件只有图标,其他可能的资源文件还有plist、json、xib等文件,视实际情况而定。

还记得podspec文件中的这两个属性么:

spec.resources       = "Resources/**/*.{xcassets,json,plist}"
spec.resource_bundle = { "QRCodeReader" => "Resources/**/*.{xcassets,json,plist}" }

首先来具体看下这两个属性分别代表什么含义,以下内容引用自Cocoapods官方文档:

resources

A list of resources that should be copied into the target bundle.

For building the Pod as a static library, we strongly recommend library developers to adopt resource bundles as there can be name collisions using the resources attribute. Moreover, resources specified with this attribute are copied directly to the client target and therefore they are not optimised by Xcode.

resource_bundles

This attribute allows to define the name and the file of the resource bundles which should be built for the Pod. They are specified as a hash where the keys represent the name of the bundles and the values the file patterns that they should include.

这俩个属性选择其中一个使用即可,二者最主要的区别在于 resource_bundles 会创建独立的 bundle,能够有效解决资源文件重名问题,也是 CocoaPods 强烈建议使用的属性。resource 属性则不会创建属于 Pod 库自己的 bundle,打包时资源文件会被拷贝到 main bundle 里。

这里我们使用resource_bundles属性,=>符号左侧表示 bundle 的名字,建议 bundle 名字中至少要包含 Pod 库的名字以最大限度的防止命名冲突。=>符号右侧是资源文件的路径。

首先在 QRCodeReader 根目录下创建Resources文件夹,用于存放资源文件,并在其中创建一个Assets.xcassets文件夹用于管理图标资源。之后在主工程运行pod install,打开工程后可以看见在QRCodeReader目录下面多了一个Assets,把组件用到的图标从主工程中移除并添加到组件库的Assets中。

这时编译运行起来后会发现这些图标没有加载成功,原因在于我们使用的UIImage(named: String)方法只会在main bundle中查找图片资源,而我们之前的操作把图标资源放到了一个单独的 bundle 中,因此在加载图片时需要指定图片所在的 bundle。

因为在Podfile中使用了use_frameworks!,组件最终会以 framework 的形式集成到 App 包中,而 bundle 文件位于 App 的"/Frameworks/QRCodeReader.framework/QRCodeReader.bundle"目录下,我们可以通过如下代码来加载指定 bundle 中的图标资源:

// App main bundle 根路径
let mainPath = Bundle.main.resourcePath 
// QRCodeReader.bundle的相对路径
let pathComponent = "/Frameworks/QRCodeReader.framework/QRCodeReader.bundle"
// 获取bundle对象
let bundle = Bundle(path: mainPath + pathComponent)
// 获取图片资源
let image = UIImage(named: imageName, in: bundle, compatibleWith: nil)

稍作封装:

func image(named: String, in bundleName: String) -> UIImage? {
    let mainPath = Bundle.main.resourcePath
    let pathComponent = "/Frameworks/\(bundleName).framework/\(bundleName).bundle"
    let bundle = Bundle(path: mainPath + pathComponent)
    if let image = UIImage(named: named, in: bundle, compatibleWith: nil) {
       return image
    } else {
       return UIImage(named: named) // 兜底策略
    }
}

如果没有使用use_frameworks!,而是采用静态库的形式集成, bundle 文件会位于 App main bundle 的根目录下。此时上述代码的pathComponent应该修改为:

let pathComponent = "/\(bundleName).bundle"

其余部分不变。

为了能够兼容使用静态库和使用动态库两种情况,我们把两种情况下加载图片的代码合并处理:

public func image(named: String, in bundleName: String) -> UIImage? {
    if let image = _dynamicImage(named: named, in: bundleName) {
        return image
    } else if let image = _staticImage(named: named, in: bundleName) {
        return image
    } else {
        return UIImage(named: named)
    }
}

private func _staticImage(named: String, in bundleName: String) -> UIImage? {
    let pathComponent = "/\(bundleName).bundle"
    return _image(named: named, with: pathComponent)
}

private func _dynamicImage(named: String, in bundleName: String) -> UIImage? {
    let pathComponent = "/Frameworks/\(bundleName).framework/\(bundleName).bundle"
    return _image(named: named, with: pathComponent)
}

private func _image(named: String, with pathComponent: String) -> UIImage? {
    guard let mainPath = Bundle.main.resourcePath else { return nil }

    let path = mainPath + pathComponent
    let bundle = Bundle(path: path)
    return UIImage(named: named, in: bundle, compatibleWith: nil)
}

对于其他类型资源文件的加载,大家可参考图片加载方式自行实现,这里就不一一介绍了。

到此为止,一个最基本的组件化改造实践就完成了,由于案例中的QRCodeReader作为演示,本身体量较小且和外部耦合性不强,整个改造过程比较顺利。实际项目中尤其是对业务进行组件化改造时,往往需要处理复杂的依赖关系,以及管理类和接口的访问级别。

6.子组件

前文已经提到了,QRCodeReader本身体量很小,实际开发中不足以单独成立一个组件。但是又存在将其独立出来的必要性,这种情况在项目中并不少见。这时,我们可以将它们配置成sub-specifications(暂且称其为子组件),并放到同一个组件库中。

specification提供了subspec属性专门用来配置 sub-specifications,下面是Cocoapods官网对于subspec属性的介绍:

subspec

Represents specification for a module of the library.


Subspecs participate on a dual hierarchy.

On one side, a specification automatically inherits as a dependency all it children ‘sub-specifications’ (unless a default subspec is specified).

On the other side, a ‘sub-specification’ inherits the value of the attributes of the parents so common values for attributes can be specified in the ancestors.

大意为:subspec描述了该组件库中的一个子组件。具有两层含义,一是,除非特殊说明,所有的子组件都是依赖其父组件;二是,子组件继承了父组件的属性值。

从描述中可以看出,一个组件内可以包含若干个子组件。以《跟客宝》为例,除了QRCodeReader,我们将项目中其他的功能组件也都独立了出来放到统一的父组件内,并将这些功能组件配置成为子组件,父组件命名为FocusUtility

FocusUtility.podspec文件中添加以下配置:

spec.subspec 'QRCodeReader' do |reader|
    reader.source_files = 'QRCodeReader/**/*.{swift,m,c,h}'
end

spec.subspec 'ImageViewer' do |viewer|
    viewer.source_files = 'ImageViewer/**/*.{swift,m,c,h}'
end

spec.subspec 'ImagePicker' do |picker|
    picker.source_files = 'ImagePicker/**/*.{swift,m,c,h}'
end

spec.subspec 'DateRangePicker' do |picker|
    picker.source_files = 'DateRangePicker/**/*.{swift,m,c,h}'
end

使用spec.subspec来声明一个子组件,并配置它的源文件路径。这里声明了四个子组件,无论是父组件还是子组件,都需要对应的podspec文件,子组件的podspec文件编写方式与之前介绍的一致,此处不再介绍。

在集成FocusUtility组件时,可以选择集成它的全部子组件,也可以只集成它的部分子组件。

# 集成所有子组件
pod 'FocusUtility', :path => '../FocusUtility'
# 仅集成其中的 QRCodeReader 组件
pod 'FocusUtility/QRCodeReader', :path => '../FocusUtility'

这样就把零碎的功能组件集中起来,相较于每个小组件都制作成独立组件,子组件的方式无疑降低了管理复杂度。

7.结束语

到此为止,一个最简单的组件化实践流程就结束了。为什么说是最简单的呢?

  1. 组件体量小;

  2. 不存在复杂依赖关系;

  3. 仅考虑 Swift 实现,未加入 OC 支持;

  4. 一人对项目实施组件化后,未考虑如何让组内其他人也快速的完成组件化;

  5. 没有组件的版本管理;

  6. 没有组件间的依赖关系管理等。

这些都是实施一个完整的组件化方案所必须的,不过组件化是一个循序渐进的过程,方案的内容也会不断地更新和完善。本篇仅作为组件化探索的初级方案供大家参考,随着项目组件化的不断深入,针对上述的几个问题再为大家带来更加完善的管理方案!

8.参考文档

1.https://cocoapods.org

2.https://dreampiggy.com/2018/11/26/CocoaPods的资源管理和Asset%20Catalog优化/

你可能感兴趣的:(ios,swift,objective-c,xcode,开发语言)