iOS组件化实践之:新建一个Pod组件库

Posted by PaysonChen on July 26, 2023

iOS组件化实践之:新建一个Pod组件库

0.背景

在IOS组件化的实践过程中,

  • 规划期间需要对组件进行定义、功能划分、接口设计等
  • 实践期间需要对组件进行创建,迁移主工程能力等

这过程,就看你需要较为频繁地创建Pod组件库,因此对于了解一下,Pod组件的创建,及其模板的使用与修改,变成较为重要

1. 通过常规模板创建

CocoaPods提供的创建指令:

1
pod lib create xx

通过输出可以看到:

1
Cloning `https://github.com/CocoaPods/pod-template.git` into `xx`.

此命令默认从 github Pod-template,下载默认模板进行创建,依次回答几个问题之后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 ]
 > 
quick
Would you like to do view based testing? [ Yes / No ]
 > 
yes

当前路径下就生成了一个Pod仓库,Spec文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Pod::Spec.new do |s|
  s.name             = 'xx'
  s.version          = '0.1.0'
  s.summary          = 'A short description of xx.'

# 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/chenyp/xx'
  # s.screenshots     = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  s.author           = { 'chenyp' => '165685965@qq.com' }
  s.source           = { :git => 'https://github.com/chenyp/xx.git', :tag => s.version.to_s }
  # s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'

  s.ios.deployment_target = '10.0'

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

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

2. 通过自定义模板创建

一般而言,不建议通过上述方式进行创建,默认以git全局配置的name、email字段,加上github主页进行填充,因为默认模板所创建的诸多字段是必要修改的(不改无法通过私有组件部署):

1
2
3
4
  s.homepage         = 'https://github.com/chenyp/xx'
  s.author           = { 'chenyp' => '165685965@qq.com' }
  s.source           = { :git => 'https://github.com/chenyp/xx.git', :tag => s.version.to_s }
……

由于组件化的过程是需要频繁创建多个组件仓库,如果每次都改上述字段,有可能错改漏改,导致效率低下,基于懒惰是程序员的美德这一不成文的追求(其实应该是尽量能把重复工作流程化),着手对上述模板进行修改。

自定义模板创建:

1
git clone git@github.com:CocoaPods/pod-template.git

通过修改仓库中的“NAME.podspec”文件中的相关字段

1
2
3
4
  s.homepage         = 'https://内网githost/name/xx'
  s.author           = { '内网git提交commit姓名' => '内网git提交commit email' }
  s.source           = { :git => 'https://内网githost/name/xx.git', :tag => s.version.to_s }	#clone 组件库地址
……

修改后将上述模板放到git仓库上,执行:

1
 pod lib create xx  --template-url=ssh://内网githost/ios/Pods/Tmplate/XXX_Pod_Tmeplate.git

3. 通过高度自定义模板创建

一般而言,到上述即可完成基础的模板自定义,减少创建私有库后所需的重复修改字段的工作量,但是对于下列选项的一致性,如果能省略冗余及提高统一性,自然更好了:

  • 平台选择的冗余(iOS)

  • 代码风格的统一(类名前缀)

  • 语言统一(iOS/Swift)

  • Demo工程

  • 测试依赖

这就需要对pod-template的ruby代码进行修改已达到不需要重复设定模板交互的目的:

模板配置:TemplateConfigurator.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    def run
      @message_bank.welcome_message

      platform = self.ask_with_answers("What platform do you want to use?", ["iOS", "macOS"]).to_sym

      case platform
        when :macos
          ConfigureMacOSSwift.perform(configurator: self)
        when :ios
          framework = self.ask_with_answers("What language do you want to use?", ["Swift", "ObjC"]).to_sym
          case framework
            when :swift
              ConfigureSwift.perform(configurator: self)

            when :objc
              ConfigureIOS.perform(configurator: self)
          end
      end
      
      #……此处省略后面的代码……#
    end

改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    def run
      @message_bank.welcome_message

      #自定义模板优化:这里可以绕过了一些问题(免回答iOS、Objc),当然如果想要设置成其他的,也可以调用其他方法,或者恢复询问式配置
      ConfigureIOS.perform(configurator: self)
      
#      platform = self.ask_with_answers("What platform do you want to use?", ["iOS", "macOS"]).to_sym
#
#      case platform
#        when :macos
#          ConfigureMacOSSwift.perform(configurator: self)
#        when :ios
#          framework = self.ask_with_answers("What language do you want to use?", ["Swift", "ObjC"]).to_sym
#          case framework
#            when :swift
#              ConfigureSwift.perform(configurator: self)
#
#            when :objc
#              ConfigureIOS.perform(configurator: self)
#          end
#      end

      #……此处省略后面的代码……#
    end

选择了相关语言,就在相关语言的 Configure 内配置后续的设置,例如:选择了iOS,就在ConfigureIOS.rb配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
    def perform

      keep_demo = configurator.ask_with_answers("Would you like to include a demo application with your library", ["Yes", "No"]).to_sym

      framework = configurator.ask_with_answers("Which testing frameworks will you use", ["Specta", "Kiwi", "None"]).to_sym
      case framework
        when :specta
          configurator.add_pod_to_podfile "Specta"
          configurator.add_pod_to_podfile "Expecta"

          configurator.add_line_to_pch "@import Specta;"
          configurator.add_line_to_pch "@import Expecta;"

          configurator.set_test_framework("specta", "m", "ios")

        when :kiwi
          configurator.add_pod_to_podfile "Kiwi"
          configurator.add_line_to_pch "@import Kiwi;"
          configurator.set_test_framework("kiwi", "m", "ios")

        when :none
          configurator.set_test_framework("xctest", "m", "ios")
      end

      snapshots = configurator.ask_with_answers("Would you like to do view based testing", ["Yes", "No"]).to_sym
      case snapshots
        when :yes
          configurator.add_pod_to_podfile "FBSnapshotTestCase"
          configurator.add_line_to_pch "@import FBSnapshotTestCase;"

          if keep_demo == :no
              puts " Putting demo application back in, you cannot do view tests without a host application."
              keep_demo = :yes
          end

          if framework == :specta
              configurator.add_pod_to_podfile "Expecta+Snapshots"
              configurator.add_line_to_pch "@import Expecta_Snapshots;"
          end
      end

      prefix = nil

      loop do
        prefix = configurator.ask("What is your class prefix").upcase

        if prefix.include?(' ')
          puts 'Your class prefix cannot contain spaces.'.red
        else
          break
        end
      end
	      #……此处省略后面的代码……#
  end


找到相关的问答区:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
    def perform
      #自定义模板优化: 这里默认需要demo,当然如果想要设置成其他的,也可以调用其他方法,或者恢复询问式配置
      keep_demo = "yes"#configurator.ask_with_answers("Would you like to include a demo application with your library", ["Yes", "No"]).to_sym
      #自定义模板优化:: 这里默认不需要testing frameworks,当然如果想要设置成其他的,也可以调用其他方法,或者恢复询问式配置
      framework = "none" #configurator.ask_with_answers("Which testing frameworks will you use", ["Specta", "Kiwi", "None"]).to_sym
      case framework
        when :specta
          configurator.add_pod_to_podfile "Specta"
          configurator.add_pod_to_podfile "Expecta"

          configurator.add_line_to_pch "@import Specta;"
          configurator.add_line_to_pch "@import Expecta;"

          configurator.set_test_framework("specta", "m", "ios")

        when :kiwi
          configurator.add_pod_to_podfile "Kiwi"
          configurator.add_line_to_pch "@import Kiwi;"
          configurator.set_test_framework("kiwi", "m", "ios")

        when :none
          configurator.set_test_framework("xctest", "m", "ios")
      end

      #自定义模板优化:这里默认不需要view based testing,当然如果想要设置成其他的,也可以调用其他方法,或者恢复询问式配置
      snapshots = "no"#configurator.ask_with_answers("Would you like to do view based testing", ["Yes", "No"]).to_sym
      case snapshots
        when :yes
          configurator.add_pod_to_podfile "FBSnapshotTestCase"
          configurator.add_line_to_pch "@import FBSnapshotTestCase;"

          if keep_demo == :no
              puts " Putting demo application back in, you cannot do view tests without a host application."
              keep_demo = :yes
          end

          if framework == :specta
              configurator.add_pod_to_podfile "Expecta+Snapshots"
              configurator.add_line_to_pch "@import Expecta_Snapshots;"
          end
      end

      prefix = nil

      loop do
      #自定义模板优化:这里默认prefix为PSC,当然如果想要设置成其他的,也可以调用其他方法,或者恢复询问式配置
        prefix = "PSC"#configurator.ask("What is your class prefix").upcase

        if prefix.include?(' ')
          puts 'Your class prefix cannot contain spaces.'.red
        else
          break
        end
      end
      #……此处省略后面的代码……#
  end

至此,优化的自定义模板已经完成,再创建另一个(如果你需要保留上面那种简易自定义)git仓库存储,一键创建更符合需求的pod私有库模板:let‘s try

1
 pod lib create xx  --template-url=ssh://内网githost/ios/Pods/Tmplate/PSC_iOS_Objc_Demo_NoneTest.git

4. 如果你还是使用Gem管理Pod插件的话

一般而言,在多人团队中,需要用到Gem来管理Pod插件版本,避免由于Pod版本不一致,导致的集成与编译问题,如果你还有Gemfile需要配置的话,由于模板配置的代码最后的是:

1
2
3
4
5
6
7
8
      # There has to be a single file in the Classes dir
      # or a framework won't be created, which is now default
      `touch Pod/Classes/ReplaceMe.m`

      `mv ./templates/ios/* ./`

      # remove podspec for osx
      `rm ./NAME-osx.podspec`

在Podfile同级目录下也存放一个自己在用的Gemfile即可:

1
2
3
4
5
6
7
8
9
10
# frozen_string_literal: true

source "https://rubygems.org"

gem 'cocoapods', '1.11.2'

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

# gem "rails"

5 最后的忽略

模板创建之后,会执行Pod install,此时与常规工程一样,在Example下会新增Pod文件夹存在Pod依赖库,提交时需忽略,本着能少干重复劳动就不多干的原则,我们再把.gitignore修改一下:

在根目录中找到.gitignore文件,由于是.开头,默认是隐藏了,在下面代码中添加你需要忽略的文件夹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# macOS
.DS_Store

# Xcode
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata/
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa

# Bundler
.bundle

# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts

Carthage/Build

# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control
# 
# Note: if you ignore the Pods directory, make sure to uncomment
# `pod install` in .travis.yml
#
# Pods/ #打开此处注释可以忽略 以下Example/Pods/

#以下可以继续添加忽略文件及文件夹

6.扩展:Podfile依赖

一般而言,创建的Pod私有库中的Podfile,会包含简单的模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
use_frameworks!

platform :ios, '10.0'

target '${POD_NAME}_Example' do
  pod '${POD_NAME}', :path => '../'

  target '${POD_NAME}_Tests' do
    inherit! :search_paths

    ${INCLUDED_PODS}
  end
end

对于单个Pod组件库,也许够用了,但是我们如果是在进行组件化改造的进程中,需要与现有的私有库进行交互与依赖:比如业务库依赖基础库等,

为了方便起见,不至于对每次创建私有库进行Podfile配置(还是懒😭),因此对Podfile进行了改造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
source 'ssh://私有库git地址/Specs.git'
source 'https://github.com/CocoaPods/Specs.git'

use_frameworks!

platform :ios, '12.0'	#写这篇文章时使用的podfile默认是10.0,可以改成你需要的
post_install do |installer|
    installer.generated_projects.each do |project|
          project.targets.each do |target|
              target.build_configurations.each do |config|
                  config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
                  config.build_settings["DEVELOPMENT_TEAM"] = "xxxxx"	#这里指定签名,避免编译出错还要手动指定
               end
          end
   end
end

use_frameworks!


target '${POD_NAME}_Example' do

#新建pod库创建模板的时候把私有库拉取一下
def debug_pod(pod_name)
  this_pod_name = "${POD_NAME}"
  puts "debug_pod :pod_name = #{pod_name} and this_pod_name is #{this_pod_name}"
  if this_pod_name == pod_name
    puts "#{this_pod_name} is equal args #{pod_name} do not pod"
  elsif
    #为了方便管理,这里的所有私有库建议放在同一个目录下
    pod "#{pod_name}", :path => "../../#{pod_name}/"
  end
end

  #为了避免重复定义 这里统一按下面方式进行处理吧,由于下文已经匹配了当前pod,这里不用注释掉
  pod '${POD_NAME}', :path => '../'
    
  #这里定义需要被引入的所有私有库的库名
  private_pods = ["基础组件1", "基础组件2", "基础组件3", "能力组件1",  "能力组件2",  "能力组件3",  "业务组件1",  "业务组件2"]

  #这里将所有私有库名与路径匹配起来进行pod
  private_pods.each do |private_pod|
    debug_pod(private_pod)
  end


  #如果上述方法用不来,或者不好用也可以改用下面比较传统的方式进行:
  #pod '基础组件1', :path => '../../基础组件1/'
  #pod '基础组件2', :path => '../../基础组件2/'
  #pod '基础组件3', :path => '../../基础组件3/'

  #pod '能力组件1', :path => '../../能力组件1/'
  #pod '能力组件2', :path => '../../能力组件2/'
  #pod '能力组件3', :path => '../../能力组件3/'

  #pod '业务组件1', :path => '../../业务组件1/'
  #pod '业务组件2', :path => '../../业务组件2/'

  target '${POD_NAME}_Tests' do
    inherit! :search_paths
    ${INCLUDED_PODS}
  end
end

值得一提的是,需要把所有Dev Pod放在同一个目录下,避免路径管理麻烦

至此,生产的Pod组件库模板可以满足组件化进程需求了。

##