您当前的位置:首页 > 电脑百科 > 程序开发 > 移动端 > IOS

iOS组件化初探

时间:2022-11-21 15:06:33  来源:今日头条  作者:闪念基因

1.背景

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

 

这样做有以下几个问题:

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

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

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

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

  1. 难以维护、不可持续

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

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

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

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

2.基本原理

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

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

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

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文件和若干模板文件,如下图所示:

 

其中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' => 'lanfudong214839@sohu-inc.com' }
  s.source           = { :git => 'https://github.com/lanfudong/QRCodeReader.git', :tag => s.version.to_s }
  # s.social_media_url = 'https://Twitter.com/<TWITTER_USERNAME>'

  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 'A.NETworking', '~> 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" => "lanfudong214839@sohu-inc.com" }
  # Or just: spec.author    = "lanfudong"
  # spec.authors            = { "lanfudong" => "lanfudong214839@sohu-inc.com" }
  # 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的源码文件目前还在项目的主工程里面,若想将其独立出来,我们需要将其进行适当的修改,分为两个方面:

 

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

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

 

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

Access Levels

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

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

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

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

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

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

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

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

 

我这里使用第三种方法手动创建的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命令,看见如下输出,说明集成成功了。

 

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

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

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

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

 

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优化/

 

作者:兰福东

来源:微信公众号:搜狐技术产品

出处
:https://mp.weixin.qq.com/s/Nk2ig6SV-qG2Q90lwBOO4w



Tags:iOS   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
iOS 17.5进一步开放 游戏可官网下载直接安装
苹果iOS 17.5版本系统将在近期开放测试,在该版本中,苹果会进一步开放侧载功能,让用户能够如同安卓手机那样,随意安装应用。据外媒消息称,苹果iOS 17.5版本系统将在近期开放测试,在...【详细内容】
2024-04-02  Search: iOS  点击:(9)  评论:(0)  加入收藏
揭秘 iOS 17.4:欧盟独享功能,其他地区无法体验
【环球网科技综合报道】据外媒消息,随着iOS 17.4的发布,欧洲用户能够独享一些全球其他地区无法获得的新功能。这得益于欧洲联盟的《数字市场法案》,苹果不得不向侧载等功能开放...【详细内容】
2024-03-18  Search: iOS  点击:(16)  评论:(0)  加入收藏
苹果iOS17.4值得升级吗?续航信号评测来了
苹果iOS17.4值得升级吗?很多iPhone用户都在纠结这个版本续航和信号都有没提升,纠结是否需要升级,那么这个版本信号信号到底怎样呢?下面就给大家分享iPhone13升级iOS17.4的续航信...【详细内容】
2024-03-07  Search: iOS  点击:(11)  评论:(0)  加入收藏
苹果“安全”神话破灭?!iOS首次出现木马病毒
一直以来,iOS系统都以“安全性极高”闻名手机圈。在封闭软硬件生态加持下,iOS确实要比安卓系统更安全点。但是,小雷要说但是了嚯,最近iOS却首次出现了木马病毒。图源:苹果近日,在...【详细内容】
2024-02-19  Search: iOS  点击:(66)  评论:(0)  加入收藏
木马病毒蔓延至iOS:苹果用户如何防范?
近期,网络安全公司Group-IB发布了一份引人注目的报告,揭示了黑客已经开始针对iPhone用户发起银行木马攻击。这一事件标志着iOS系统首次遭受此类安全威胁,为全球的苹果用户敲响...【详细内容】
2024-02-18  Search: iOS  点击:(41)  评论:(0)  加入收藏
黑客利用iOS系统中的三个零日漏洞在iPhone上安装间谍软件
2月7日,据谷歌威胁分析小组(TAG)发布的报告,黑客成功利用存在于苹果iOS系统中的三个零日漏洞,在iPhone上安装了由Variston开发的间谍软件。Variston是一家位于巴塞罗那的网络公司...【详细内容】
2024-02-07  Search: iOS  点击:(58)  评论:(0)  加入收藏
一夜之间,iOS放开了侧载和NFC等限制:苹果的欧洲福利
在最新的iOS更新中,苹果似乎放宽了一些长期以来受到限制的功能。据报道,新版本的iOS开放了侧载和NFC等功能的访问,这对于欧洲地区的用户来说无疑是一个巨大的福利。侧载,即从第...【详细内容】
2024-01-26  Search: iOS  点击:(53)  评论:(0)  加入收藏
部分iPhone用户升级iOS 17.3失败 提示“无法验证更新”
【CNMO新闻】近日,CNMO从外媒获悉,部分iPhone用户在升级iOS 17.3时遇到问题,手机会提示“无法验证更新”。iOS 17.3据悉,苹果社区的一些成员和部分Reddit用户反映,他们无法下载更...【详细内容】
2024-01-26  Search: iOS  点击:(59)  评论:(0)  加入收藏
苹果iOS 17.3正式版发布:修复诸多安全漏洞
北京时间2023年1月23日凌晨,苹果向iPhone用户推送了iOS 17.3更新(内部版本号为21D50),此次更新距离上次时隔34天。iOS 17.3的安装包大小为600MB左右。尽管更新包容量不大,但iOS 1...【详细内容】
2024-01-23  Search: iOS  点击:(44)  评论:(0)  加入收藏
电脑启动故障排查:硬盘丢失、BIOS恢复失败,一篇文章帮你解决!
如果系统无法启动到Windows,可能是因为 BIOS 损坏,通常表现为No POST 或No Boot。系统的BIOS/UEFI 是硬件和软件之间的桥梁,因此如果系统无法启动,可能是 BIOS 出现故障。您可以...【详细内容】
2024-01-19  Search: iOS  点击:(54)  评论:(0)  加入收藏
▌简易百科推荐
iOS 屏幕旋转的实践解析
屏幕旋转是在视频直播类 APP 中常见的场景,在即构科技之前发布的 Roomkit SDK 中也有屏幕跟随手机自动旋转的场景。 在 Roomkit SDK 自身开发和客户接入的过程中我们也会发现...【详细内容】
2023-11-02    51CTO  Tags:iOS   点击:(199)  评论:(0)  加入收藏
iOS发布证书.p12文件无密码解决办法及导出带密码的新.p12文件方法
引言在iOS应用发布过程中,有时候会遇到使用无密码的.p12文件的情况。然而,在一些第三方平台上,可能会设置前端校验,不允许上传空密码的.p12文件。对于开发者来说,这样的情况会造...【详细内容】
2023-10-23  慕ie    Tags:iOS   点击:(204)  评论:(0)  加入收藏
解决提交到App Store时的ITMS-90478和ITMS-90062错误
正文1. 什么是ITMS-90478和ITMS-90062错误?2. 解决方法2.1 确定当前的版本号和构建号2.2 递增版本号和构建号2.3 再次尝试提交应用总结参考资料错误记录摘要:本文为iOS技术博...【详细内容】
2023-10-20  慕ie    Tags:App Store   点击:(231)  评论:(0)  加入收藏
iOS代码混淆和加固技术详解
摘要:本文介绍了iOS开发中常用的代码混淆和加固技术,包括数据加密、应用加壳和代码混淆。其中,重点讨论了代码混淆的实现方法和注意事项,并推荐了一些相关的工具和库。引言在开...【详细内容】
2023-10-17  慕ie    Tags:iOS   点击:(255)  评论:(0)  加入收藏
苹果上架c流程的详细步骤
摘要:本文将为iOS技术博主介绍苹果上架App备案流程的详细步骤,包括注册开发者账号、创建App ID、创建证书、创建Provisioning Profile、开发应用程序、提交应用程序、审核和上...【详细内容】
2023-09-08  慕ie    Tags:App备案   点击:(203)  评论:(0)  加入收藏
DDD实战 - Repository模式的妙用
大家好,我是飘渺。今天我们继续更新DDD(领域驱动设计) & 微服务系列。在之前的文章中,我们探讨了如何在DDD中结构化应用程序。我们了解到,在DDD中通常将应用程序分为四个层次,分别...【详细内容】
2023-07-07    JAVA日知录  Tags:DDD   点击:(255)  评论:(0)  加入收藏
iOS抓包最简单方案
写过爬虫的同学都知道,当我们想对App或者小程序进行抓包时,最常用的工具是Charles、Fiddler或者MimtProxy。但这些软件用起来非常复杂。特别是当你花了一两个小时把这些软件搞...【详细内容】
2023-07-07    未闻Code  Tags:iOS   点击:(234)  评论:(0)  加入收藏
iOS使用FFmpeg命令行
官方文档 FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。它提供了录制、转换以及流化音视频的完整解决方案。 FFmpeg的代码是包括两部分...【详细内容】
2023-03-23  音视频开发T哥  今日头条  Tags:FFmpeg   点击:(223)  评论:(0)  加入收藏
为了一个HTTP请求问题,差点和iOS干起来
本次斗殴事件起因全部归iOS,为啥这么说,http请求都不会发,瞎写的什么玩意(ps:他应该不会看到...)。在处理本次冲突中,意外发现了另外一个存在已久的bug,我们先说说这个玩意,再说我们...【详细内容】
2023-03-14  程序员的成长  今日头条  Tags:iOS   点击:(165)  评论:(0)  加入收藏
新手学IOS开发-APP界面布局基础开发
上一篇文章写到IOS开发环境搭建,还没学会的读者可参考IOS开发环境搭建,今天我们熟悉一下开发工具的基本使用,为了对IOS软件开发有一个基础的认识,同时提升学习兴趣,我们先实现一...【详细内容】
2023-02-28  蒲公英互联    Tags:IOS   点击:(231)  评论:(0)  加入收藏
站内最新
站内热门
站内头条