InAppSettingsKit

Build Status Version Swift Package Manager compatible Carthage compatible License Platform Sponsor Mastodon

InAppSettingsKit (IASK) 是一个开源框架,可以轻松地将应用内设置添加到您的 iOS、Catalyst 或 visionOS 应用中。通常,iOS 应用使用 Settings.bundle 资源在“设置”应用中添加特定于应用的设置。InAppSettingsKit 利用相同的 bundle,并允许您在您的应用内呈现相同的设置屏幕。因此,用户可以选择在哪里更改设置。

IASK 不仅复制了系统设置的功能集,而且还支持大量额外的元素和配置选项。

从 IASK 2.x 更新? 请阅读发行说明

它是如何工作的?

为了支持传统的 Settings.app 面板,应用程序必须包含一个 Settings.bundle,其中至少包含一个 Root.plist,以指定设置 UI 元素与 NSUserDefaults 键的连接。 InAppSettingsKit 基本上只是使用相同的 Settings.bundle 来完成其工作。这意味着当您想要包含新的设置参数时,无需进行额外的工作。只需将其添加到 Settings.bundle,它就会同时出现在应用内和 Settings.app 中。支持所有设置类型,如文本字段、滑块、切换元素、子视图等。

如何包含它?

源代码可在 github 上找到。 有几种安装方法。

使用 SPM

要使用 Swift Package Manager 安装 InAppSettingsKit,您可以按照 Apple 发布的教程,使用 InAppSettingsKit 仓库的 URL 和当前版本。

  1. 在 Xcode 中,选择“File”→“Add Packages…”
  2. 输入 https://github.com/futuretap/InAppSettingsKit.git

使用 CocoaPods

添加到您的 Podfile

pod 'InAppSettingsKit'

然后运行 pod install

使用 Carthage

添加到您的 Cartfile

github "futuretap/InAppSettingsKit" "master"

示例应用程序

InAppSettingsKit 包含一个 Xcode 示例应用程序,演示了其所有丰富的功能。 同时适用于推送和模态视图控制器。
要运行示例应用程序

  1. 从项目根文件夹中,在 Xcode 中打开 InAppSettingsKit.xcworkspace
  2. 将方案更改为 Sample App (Product > Scheme > Sample App)。
  3. 选择一个目标,例如 iPhone 模拟器。
  4. 要构建和运行应用程序,请选择 Product > Run,或单击 Xcode 工具栏中的“运行”按钮。

应用集成

为了开始使用 IASK,请将 Settings.bundle 添加到您的项目 (File -> Add File -> Settings bundle) 并使用您的设置编辑 Root.plist (请参阅 Apple 关于 Schema File Root Content 的文档)。 请继续阅读以深入了解更多高级用法。

要显示 InAppSettingsKit,请实例化 IASKAppSettingsViewController 并将其推送到导航堆栈上或将其嵌入为导航控制器的根视图控制器。

在代码中,使用 Swift

let appSettingsViewController = IASKAppSettingsViewController()
navigationController.pushViewController(appSettingsViewController, animated: true)

在代码中,使用 Swift 作为 swift 包的一部分

在模块化应用程序中,您可能希望将所有与设置相关的代码移动到一个单独的包中,并且仅在该处引用 InAppSettingsKit 依赖项。 您的 Package.swift 看起来像这样

let package = Package(
    name: "SettingsPackage",
    platforms: [.iOS(.v17)],
    dependencies: [
        .package(url: "https://github.com/futuretap/inappsettingskit", from: "3.4.0")
    ],
    .target(
        name: "SettingsPackage",
        dependencies: [
            .product(name: "InAppSettingsKit", package: "inappsettingskit"),
        ],
        resources: [
            .copy("InAppSettings.bundle")
        ]
    )
)

(请注意,InAppSettings.bundle 目录也是包的一部分,不再属于主应用程序。)

现在创建一个 IASKAppSettingsViewController 需要将其 bundle 属性设置为包的 bundle

struct InAppSettingsView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> some UIViewController {
        let iask = IASKAppSettingsViewController(style: .insetGrouped)
        iask.bundle = Bundle.module // IMPORTANT
        return iask
    }

    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { }
}

在代码中,使用 Objective-C

IASKAppSettingsViewController *appSettingsViewController = [[IASKAppSettingsViewController alloc] init];
[self.navigationController pushViewController:appSettingsViewController animated:YES];

通过情节提要

示例应用程序展示了如何连接所有内容。

其他更改

要自定义行为,请实现 IASKSettingsDelegate 并设置 IASKAppSettingsViewControllerdelegate 属性。 对于高级自定义需求,支持 IASKAppSettingsViewController 的子类化。

根据您的项目,可能需要在应用程序的启动代码中进行一些更改。 如果用户更改了设置,您的应用程序必须能够在运行时重新配置自身。 这可以在 -reconfigure 方法中完成,该方法从 -applicationDidFinishLaunching 以及 IASKAppSettingsViewController 的代理方法 -settingsViewControllerDidEnd: 中调用。

附加功能

InAppSettingsKit 的目的是创建 100% 模仿 Settings.app 的行为 (请参阅 Apple Settings Application Schema Reference)。 最重要的是,我们添加了大量奖励功能,使 IASK 更加灵活和动态。

自定义 inApp plists

设置 plists 可以依赖于设备:Root~ipad.plist 将在 iPad 上使用,而 Root~iphone.plist 将在 iPhone 上使用。 如果不存在,将使用 Root.plist

InAppSettingsKit 添加了通过使用 .inApp.plist 而不是 .plist 来覆盖这些标准文件的可能性。 或者,您可以创建一个完全独立的 bundle,名为 InAppSettings.bundle 而不是通常的 Settings.bundle。 如果您想禁止在 Settings.app 中显示设置,后一种方法很有用。

这是 plists 的完整搜索顺序

隐私链接

如果应用程序在其 Info.plist 中包含用于各种隐私功能(例如相机或位置访问)的使用密钥,则 IASK 会在根设置页面的顶部显示一个“隐私”单元格。 此单元格打开系统“设置”应用,并显示应用程序的设置面板,用户可以在其中指定应用程序的隐私设置。

如果您不想显示“隐私”单元格,请将属性 neverShowPrivacySettings 设置为 YES

示例应用程序定义了 NSMicrophoneUsageDescription 以显示单元格。 请注意,设置页面尚未显示任何隐私设置,因为应用程序实际上并未访问麦克风。 隐私设置仅在首次使用受隐私保护的 API 后才会在“设置”应用中显示。

配色方案

您可以通过在 IASKAppSettingsViewController 实例的视图上设置 tintColor 来指定它。 颜色用于按钮和居中文字 (请参阅下文)。 可选地,您可以指定 settingsViewController.colorScheme = IASKColorSchemeTinted 以将 tintColor 用于所有用户可编辑的选项,例如多值元素。

打开 URL

InAppSettingsKit 添加了一个新元素 IASKOpenURLSpecifier,允许使用外部应用程序(即 Safari 或 Mail)打开指定的 URL。 要启动的 URL 在 File 参数中指定。 有关详细信息,请参见示例 Root.inApp.plist

Web 视图控制器

要在您的应用程序内打开指定的 URL,IASKAppSettingsWebViewController 会显示一个全屏视图控制器,其中嵌入了 WKWebView
默认情况下,它会在页面加载时在导航栏的右侧显示一个不确定的活动指示器。

Web 视图控制器可以在 Settings plist 中定义,方法是使用以下强制属性

使用以下可选属性来自定义 Web 视图控制器

有关更多详细信息,请打开示例应用程序并查看所有以 WebView 开头的行。

尽管 IASKAppSettingsWebViewController 看上去可能与 SFSafariViewController 相似,但最大的区别在于 IASKAppSettingsWebViewController 不会向用户显示 URL,也不能在外部浏览器(即 Safari 或 Chrome)中打开。
换句话说,它可以保护您的源代码的私密性。

邮件编辑器

自定义的 IASKMailComposeSpecifier 元素允许通过打开邮件撰写视图从应用程序内发送邮件。您可以使用设置 plist 设置以下(可选)参数:IASKMailComposeToRecipentsIASKMailComposeCcRecipentsIASKMailComposeBccRecipentsIASKMailComposeSubjectIASKMailComposeBodyIASKMailComposeBodyIsHTML。可选地,您可以实现

- (BOOL)settingsViewController:(id<IASKViewController>)settingsViewController shouldPresentMailComposeViewController:(MFMailComposeViewController*)mailComposeViewController forSpecifier:(IASKSpecifier*)specifier;

在您的代理中,您可以自定义邮件(例如,预先填充带有动态内容的正文、添加附件)、修改撰写视图控制器的外观,甚至阻止标准呈现。如果设备上未配置电子邮件,则会显示警报。 IASKSpecifier 是定义单个设置单元格的内部模型对象。重要的 IASKSpecifier 属性包括:

按钮

InAppSettingsKit 添加了一个 IASKButtonSpecifier 元素,允许调用自定义操作。只需添加以下代理方法

- (void)settingsViewController:(IASKAppSettingsViewController*)sender buttonTappedForSpecifier:(IASKSpecifier*)specifier;

发送者始终是 IASKAppSettingsViewController 的实例,它是 UIViewController 的子类。因此,您可以访问其视图属性(可能便于显示操作表)或推送另一个视图控制器。另一个巧妙的功能是,IASK 按钮的标题可以被 NSUserDefaults(或任何其他设置存储 - 见下文)中的(可本地化的)值覆盖。这对于切换按钮(例如,登录/注销)非常方便。有关详细信息,请参见示例应用程序

默认情况下,按钮居中对齐,除非指定了图像(默认:左对齐)。默认对齐方式可能会被覆盖。

多行文本视图

与标准文本字段类似,IASKTextViewSpecifier 显示一个全宽、多行文本视图,该视图会根据输入的文本调整大小。它还支持 KeyboardTypeAutocapitalizationTypeAutocorrectionType

日期选择器

IASKDatePickerSpecifier 显示一个 UIDatePicker 以设置日期和/或时间。它支持以下选项:

有 3 个可选的代理方法可以自定义如何存储和显示日期和时间:

- (NSDate*)settingsViewController:(IASKAppSettingsViewController*)sender dateForSpecifier:(IASKSpecifier*)specifier;

如果您以 NSDate 对象以外的自定义格式存储日期/时间,请实现此方法。当用户通过选择日期/时间选择器上方的标题单元格来开始编辑日期/时间时调用。

- (NSString*)settingsViewController:(IASKAppSettingsViewController*)sender datePickerTitleForSpecifier:(IASKSpecifier*)specifier;

实现此方法以自定义日期/时间选择器上方的标题单元格中显示的值。

- (void)settingsViewController:(IASKAppSettingsViewController*)sender setDate:(NSDate*)date forSpecifier:(IASKSpecifier*)specifier;

如果您以 NSDate 对象以外的自定义格式存储日期/时间,请实现此方法。当用户使用选择器更改日期/时间值时调用。

列表组

列表组 (IASKListGroupSpecifier) 是一项仅限 IASK 的功能,允许您管理可变数量的项目,包括添加和删除项目。标签、帐户、名称数组是典型的用例。列表组由可变数量的 ItemSpecifier 项目组成。这些项目的数量由 NSUserDefaults(或您的自定义设置存储)中的实际内容决定。换句话说,ItemSpecifier 定义单元格的类型,而单元格的数量及其内容来自 NSUserDefaults 或您的存储。如果 Deletable 参数设置为 YES,则可以通过滑动删除单元格。

可选地,列表组还有一个 AddSpecifier,用于控制列表组部分的最后一个项目。它用于添加项目,可以是文本字段、切换、滑块或子窗格。前三个在编辑完成后创建一个新项目,而子窗格则呈现一个模态子视图控制器来配置一个复杂项目,并将其保存为字典。这种子窗格的工作方式与普通的子窗格非常相似,但有一些区别:它们不是通过推送而是以模态方式呈现,并且在导航栏中具有“取消”和“完成”按钮。通过点击“完成”按钮创建一个新项目。

您可能需要指定一些在启用“完成”按钮之前需要满足的验证规则。这可以通过代理方法来实现:

- (BOOL)settingsViewController:childPaneIsValidForSpecifier:contentDictionary:

当从此方法返回 false 时,“完成”按钮将被禁用。另请注意,contentDictionary 是一个可变字典。如果您更改某些值,UI 将反映这一点。这允许您自动更正无效设置。

自定义视图

您可以使用类型 IASKCustomViewSpecifier 在 InAppSettingsKit 中指定自己的 UITableViewCell。在这种情况下,强制性字段是 Key 属性。此外,您必须支持 IASKSettingsDelegate 协议并实现以下方法:

- (CGFloat)settingsViewController:(UITableViewController<IASKViewController> *)settingsViewController heightForSpecifier:(IASKSpecifier *)specifier;
- (UITableViewCell*)settingsViewController:(UITableViewController<IASKViewController> *)settingsViewController cellForSpecifier:(IASKSpecifier*)specifier;

这两个方法都会为您的所有 IASKCustomViewSpecifier 条目调用。要区分它们,您可以使用 specifier.key 访问 Key 属性。在第一个方法中,您返回单元格的高度,在第二个方法中,返回单元格本身。您应该像在表视图编程中一样使用可重用的 UITableViewCell 对象。演示应用程序中有一个示例。

可选地,您可以实现

- (void)settingsViewController:(IASKAppSettingsViewController*)settingsViewController didSelectCustomViewSpecifier:(IASKSpecifier*)specifier;

捕获自定义视图的点击事件。

如果您指定 FileIASKViewControllerClassIASKViewControllerStoryBoardIdIASKSegueIdentifier(见下文),则自定义视图的选择行为与子窗格相同,并且在选择时不调用委托。

节头和页脚

Group 元素的 FooterText 键在系统设置中可用。InAppSettingsKit 也支持它。最重要的是,我们也支持 Multi Value 元素的这个键。页脚文本显示在多值选项表下方。

您可以通过添加 Key 属性并在您的 IASKSettingsDelegate 中实现以下方法,来定义 PSGroupSpecifier 段的自定义标题视图:

- (UIView *)settingsViewController:(id<IASKViewController>)settingsViewController tableView:(UITableView *)tableView viewForHeaderForSection:(NSInteger)section;

您可以通过实现以下方法来调整标题的高度:

- (CGFloat)settingsViewController:(id<IASKViewController>)settingsViewController tableView:(UITableView*)tableView heightForHeaderForSection:(NSInteger)section;

对于更简单的标题自定义,而无需自定义视图,并且如果尚未实现 -settingsViewController:tableView:viewForHeaderForSection: 方法或该方法返回部分的 nil,请实现以下方法:

- (NSString *)settingsViewController:(id<IASKViewController>)settingsViewController tableView:(UITableView*)tableView titleForHeaderForSection:(NSInteger)section;

如果该方法返回 nil 或长度为 0 的字符串,将使用 .plist 中定义的标题。

此行为类似于自定义表视图单元格。当实现方法时,如果需要,可以通过索引方便地从其索引检索部分键:

NSString *key = [settingsViewController.settingsReader keyForSection:section];

查看演示应用程序以获取具体示例。

对于页脚自定义,可以类似地实现 IASKSettingsDelegate 协议中的三个方法。

扩展子面板

自定义 ViewControllers

对于子窗格元素 (PSChildPaneSpecifier),Apple 需要一个 file 键,用于指定子 plist。InAppSettingsKit 允许选择性地指定 IASKViewControllerClassIASKViewControllerSelector。在这种情况下,通过实例化指定类的 UIViewController 子类并使用 IASKViewControllerSelector 中指定的 init 方法对其进行初始化来显示子窗格。选择器必须有两个参数:一个是 Settings bundle 中的文件名的 NSString 参数,另一个是 IASKSpecifier。然后,将自定义视图控制器推送到导航堆栈上。有关更多详细信息,请参见示例应用程序

从 StoryBoard 使用自定义 ViewControllers

或者,指定 IASKViewControllerStoryBoardId 以从主storyboard启动一个viewcontroller。指定 IASKViewControllerStoryBoardFile 以使用应用 Info.plist 中主 storyboard 以外的 storyboard。

执行 Segues

作为子窗格元素 (PSChildPaneSpecifier) 的 IASKViewControllerClassIASKViewControllerSelector 的替代方法,InAppSettingsKit 能够通过执行在您的 storyboard 中定义的任何 segue 来导航到另一个视图控制器。为此,请在 IASKSegueIdentifier 中指定 segue 标识符。

扩展各种 specifiers

副标题

IASKSubtitle 键允许为以下元素定义副标题:Toggle、ChildPane、OpenURL、MailCompose、Button。使用副标题意味着左对齐。如果可用且未指定 IASKSubtitle,则子窗格将其值显示为副标题。副标题可以是可本地化的字符串或具有可本地化的副标题的字典,具体取决于当前值。YESNO 用作布尔切换值的键。该字典可能包含一个 __default__ 键,用于在没有匹配的键时定义副标题。

文本对齐方式

对于某些元素类型,可以添加具有以下值的 IASKTextAlignment 属性以覆盖默认对齐方式:

可变字体大小

默认情况下,设置表中的标签以可变的字体大小显示,尤其方便挤入长本地化(注意:如果标签太长,这可能会破坏 Settings.app 中的外观!)。要禁用此行为,请添加一个值为 NOIASKAdjustsFontSizeToFitWidth 布尔属性。

图标

所有元素类型(滑块已经具有 MinimumValueImage 除外)都支持单元格左侧的图标图像。您可以在可选的 IASKCellImage 属性中指定图像名称。自动附加“.png”或“@2x.png”后缀,并在项目中搜索。可选地,您可以在项目中添加带有后缀“Highlighted.png”或“Highlighted@2x.png”的图像,当选择单元格时(对于 Buttons 和 ChildPanes),它将自动用作高亮图像。如果未在项目中找到该图像作为资源,InAppSettingsKit 将回退到 SF Symbols。

扩展文本字段

占位符

IASKPlaceholder 键允许为 TextField 和 TextView (IASKTextViewSpecifier) 定义占位符。

内容类型

为了支持基于内容类型的自动填充,请添加 IASKTextContentType 键,该键接受 UITextContentType 的(无前缀)常量名称。示例:要使用 UITextContentTypeEmailAddress 配置文本字段,请使用 IASKTextContentType: EmailAddress

验证

可以使用代理回调来验证文本字段:

- (IASKValidationResult)settingsViewController:(IASKAppSettingsViewController*)settingsViewController validateSpecifier:(IASKSpecifier*)specifier textField:(IASKTextField*)textField previousValue:(nullable NSString*)previousValue replacement:(NSString* _Nonnull __autoreleasing *_Nullable)replacement;

回调函数接收 IASKTextField,它是 UITextField 的子类,允许在出现验证错误时(例如,红色文本)设置文本字段的样式。 它包含一个 replacement out 参数来替换无效文本。 返回 IASKValidationResultFailedWithShake 会让文本字段抖动,以直观地指示验证错误。

自定义开关

PSToggleSwitchSpecifier 开关默认使用 UISwitch。 通过指定选项 IASKToggleStyle: Checkmark,选定的键会显示复选标记。

动态多值列表

多值列表 (PSMultiValueSpecifier) 和单选组 (PSRadioGroupSpecifier) 可以从代理动态获取它们的值和标题,而不是从静态的 Plist 文件中获取。 在您的 IASKSettingsDelegate 中实现以下两个方法。

- (NSArray*)settingsViewController:(IASKAppSettingsViewController*)sender valuesForSpecifier:(IASKSpecifier*)specifier;
- (NSArray<NSString*>*)settingsViewController:(IASKAppSettingsViewController*)sender titlesForSpecifier:(IASKSpecifier*)specifier;

示例应用程序返回所有国家/地区代码的列表作为值,以及本地化的国家/地区名称作为标题。

可以通过在 Plist 中添加 true 的 Boolean 值 DisplaySortedByTitle 键,按字母顺序对多值列表进行排序。 可以为多值列表条目提供图像。 通过 IconNames 属性(在 Values/Titles/ShortTitles 等旁边)指定图像。 多值列表支持 IASKQuickMultiValueSelection boolean 键。 如果设置为 true,子视图控制器将在选择后弹出,因此无需点击返回按钮。

设置存储

IASK 的默认行为是将设置存储在 [NSUserDefaults standardUserDefaults] 中。 但是,可以通过在 IASKAppSettingsViewController 上设置 settingsStore 属性来更改此行为。 IASK 带有两个存储实现:IASKSettingsStoreUserDefaults(默认实现)和 IASKSettingsStoreFile,它们在您选择路径的文件中读取和写入设置。 如果您需要更具体的内容,您也可以选择创建自己的存储。 创建自己的存储的最简单方法是创建 IASKAbstractSettingsStore 的子类。 只需要重写 3 个方法。 有关更多详细信息,请参见 IASKSettingsStore.{h,m}

通知

对于每个更改的设置键,都会发送 IASKSettingChangedNotification 通知。 该通知的 object 是发送视图控制器,userInfo 字典包含受影响键的键和新值。

动态单元格隐藏

有时,选项之间相互依赖。 例如,您可能想要有一个“自动连接”开关,并允许用户在启用时设置用户名和密码。 要对特定设置的更改做出反应,请使用上面解释的 IASKSettingChangedNotification 通知。

要隐藏一组单元格,请使用

- (void)[IASKAppSettingsViewController setHiddenKeys:(NSSet*)hiddenKeys animated:(BOOL)animated];

或者使用非动画版本

@property (nonatomic, strong) NSSet *hiddenKeys;

有关更多详细信息,请参见示例应用程序。 在 hiddenKeys 中包含 PSGroupSpecifier 键会隐藏整个节。

注册默认值

设置属性列表支持 DefaultValue 参数,以便在 NSUserDefaults 中未存储任何值的情况下显示默认值。 但是,当应用程序向 NSUserDefaults 查询该值时,该默认值不会被传播。 这是有道理的,因为 NSUserDefaults 不知道设置属性列表。

要为各种设置键初始设置值,NSUserDefaults 提供了 registerDefaults: 方法,该方法采用一个“回退”值字典,如果未存储任何值,则从 NSUserDefaults 返回这些值。 这通常在应用程序启动时调用。

但是,创建和维护该字典可能很麻烦,并且存在该字典和设置默认值不同步的风险。

为了解决这个问题,IASKSettingsReader 提供了一种方法,通过遍历 Root.plist 和所有子 Plist 并收集所有键的 DefaultValue 来生成该字典。

NSDictionary *defaultDict = [appSettingsViewController.settingsReader gatherDefaultsLimitedToEditableFields:YES];
[NSUserDefaults.standardUserDefaults registerDefaults:defaultDict];

iCloud 同步

要将您的 NSUserDefaults 与 iCloud 同步,还有一个名为 FTiCloudSync 的项目,该项目被实现为 NSUserDefaults 上的一个类别:所有写入和删除请求都会自动转发到 iCloud,并且来自 iCloud 的所有更新都会自动存储在 NSUserDefaults 中。 如果使用基于标准 NSUserDefaults 的存储,InAppSettingsKit 会自动更新 UI。

支持

请不要使用 Github issues 提交支持请求,我们会关闭它们。 请在 StackOverflow 上使用标签 inappsettingskit 发布您的疑问。

许可证

我们根据宽松的 BSD 许可证发布了代码,以便可以将其包含在每个项目中,无论是免费的还是付费的应用程序。 我们唯一的要求是给原始开发人员一些 credit。 包含 credits 的最简单方法是在代码中保留“Powered by InAppSettingsKit”声明。 如果您决定删除此声明,那么在 App Store 描述页面或主页上明显提及也是可以的。

作者

最初由 Luc Vandal 开发,Ortwin Gentz (Mastodon) 接管了开发并继续更新该框架。 InAppSettingsKit 在 FutureTapWhere To? 应用程序中使用,因此我们身体力行!

需要赞助商

如果您想支持我的开源工作,请考虑作为 赞助商 加入我! 💪️ 您的赞助使我能够花更多的时间在 InAppSettingsKit 和其他社区项目上。 谢谢!