- 创建分组

- 锁App
This commit is contained in:
linshujie 2026-08-12 17:59:19 +08:00
parent 5098bcda0e
commit a3e4638341
81 changed files with 4222 additions and 441 deletions

1
.gitignore vendored
View File

@ -47,4 +47,5 @@ npm-debug.log
screenshot/ screenshot/
.opencode/ .opencode/
.cursor/ .cursor/
.codex/
openspec/ openspec/

View File

@ -0,0 +1,264 @@
//
// AppRestrictShared.swift
// Shared between main app and Screen Time extensions.
//
import Foundation
import FamilyControls
import ManagedSettings
import UIKit
enum AppRestrictShared {
static let appGroupId = "group.cn.zuomeng.jisuloca"
static let storeName = ManagedSettingsStore.Name("appRestrict")
static let activityName = "appRestrict.monitoring"
static let selectionKey = "appRestrict.selection"
static let enabledTokensKey = "appRestrict.enabledTokens"
static let linksKey = "appRestrict.links"
static let shieldConfigKey = "appRestrict.shieldConfig"
static let customImageFileName = "shield_custom.jpg"
static var defaults: UserDefaults {
UserDefaults(suiteName: appGroupId) ?? .standard
}
static var containerURL: URL? {
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupId)
}
static var customImageURL: URL? {
containerURL?.appendingPathComponent(customImageFileName)
}
static func makeStore() -> ManagedSettingsStore {
ManagedSettingsStore(named: storeName)
}
}
struct AppRestrictLinkRecord: Codable, Equatable {
let catalogId: String
let tokenData: Data
let displayName: String?
let iconURL: String?
init(catalogId: String, tokenData: Data, displayName: String? = nil, iconURL: String? = nil) {
self.catalogId = catalogId
self.tokenData = tokenData
self.displayName = displayName
self.iconURL = iconURL
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
catalogId = try container.decode(String.self, forKey: .catalogId)
tokenData = try container.decode(Data.self, forKey: .tokenData)
displayName = try container.decodeIfPresent(String.self, forKey: .displayName)
iconURL = try container.decodeIfPresent(String.self, forKey: .iconURL)
}
}
struct AppRestrictShieldConfig: Codable, Equatable {
enum ImageSource: String, Codable {
case presetDefault
case presetFocus
case presetCalm
case album
}
var title: String
var subtitle: String
var primaryButtonLabel: String
var imageSource: ImageSource
static let `default` = AppRestrictShieldConfig(
title: "该应用已被锁定",
subtitle: "专注当下,稍后再回来吧",
primaryButtonLabel: "知道了",
imageSource: .presetDefault
)
}
enum AppRestrictTokenCodec {
static func encode(_ token: ApplicationToken) -> Data? {
try? PropertyListEncoder().encode(token)
}
static func decode(_ data: Data) -> ApplicationToken? {
try? PropertyListDecoder().decode(ApplicationToken.self, from: data)
}
static func encodeSelection(_ selection: FamilyActivitySelection) -> Data? {
try? PropertyListEncoder().encode(selection)
}
static func decodeSelection(_ data: Data) -> FamilyActivitySelection? {
try? PropertyListDecoder().decode(FamilyActivitySelection.self, from: data)
}
static func encodeTokenSet(_ tokens: Set<ApplicationToken>) -> Data? {
let datas = tokens.compactMap { encode($0) }
return try? PropertyListEncoder().encode(datas)
}
static func decodeTokenSet(_ data: Data) -> Set<ApplicationToken> {
guard let datas = try? PropertyListDecoder().decode([Data].self, from: data) else {
return []
}
return Set(datas.compactMap { decode($0) })
}
}
enum AppRestrictSharedStore {
static var selection: FamilyActivitySelection {
get {
guard let data = AppRestrictShared.defaults.data(forKey: AppRestrictShared.selectionKey),
let value = AppRestrictTokenCodec.decodeSelection(data) else {
return FamilyActivitySelection()
}
return value
}
set {
if let data = AppRestrictTokenCodec.encodeSelection(newValue) {
AppRestrictShared.defaults.set(data, forKey: AppRestrictShared.selectionKey)
}
}
}
static var enabledTokens: Set<ApplicationToken> {
get {
guard let data = AppRestrictShared.defaults.data(forKey: AppRestrictShared.enabledTokensKey) else {
return []
}
return AppRestrictTokenCodec.decodeTokenSet(data)
}
set {
if let data = AppRestrictTokenCodec.encodeTokenSet(newValue) {
AppRestrictShared.defaults.set(data, forKey: AppRestrictShared.enabledTokensKey)
} else {
AppRestrictShared.defaults.removeObject(forKey: AppRestrictShared.enabledTokensKey)
}
}
}
static var links: [AppRestrictLinkRecord] {
get {
guard let data = AppRestrictShared.defaults.data(forKey: AppRestrictShared.linksKey),
let value = try? JSONDecoder().decode([AppRestrictLinkRecord].self, from: data) else {
return []
}
return value
}
set {
if let data = try? JSONEncoder().encode(newValue) {
AppRestrictShared.defaults.set(data, forKey: AppRestrictShared.linksKey)
}
}
}
static var shieldConfig: AppRestrictShieldConfig {
get {
guard let data = AppRestrictShared.defaults.data(forKey: AppRestrictShared.shieldConfigKey),
let value = try? JSONDecoder().decode(AppRestrictShieldConfig.self, from: data) else {
return .default
}
return value
}
set {
if let data = try? JSONEncoder().encode(newValue) {
AppRestrictShared.defaults.set(data, forKey: AppRestrictShared.shieldConfigKey)
}
}
}
static func linkRecord(for token: ApplicationToken) -> AppRestrictLinkRecord? {
guard let data = AppRestrictTokenCodec.encode(token) else { return nil }
return links.first(where: { $0.tokenData == data })
}
static func catalogId(for token: ApplicationToken) -> String? {
linkRecord(for: token)?.catalogId
}
static func setLink(
catalogId: String,
token: ApplicationToken,
displayName: String? = nil,
iconURL: String? = nil
) {
guard let data = AppRestrictTokenCodec.encode(token) else { return }
var list = links.filter { $0.tokenData != data && $0.catalogId != catalogId }
list.append(AppRestrictLinkRecord(
catalogId: catalogId,
tokenData: data,
displayName: displayName,
iconURL: iconURL
))
links = list
}
static func removeLink(for token: ApplicationToken) {
guard let data = AppRestrictTokenCodec.encode(token) else { return }
links = links.filter { $0.tokenData != data }
}
static func applyShield(for tokens: Set<ApplicationToken>) {
let store = AppRestrictShared.makeStore()
if tokens.isEmpty {
store.clearAllSettings()
} else {
store.shield.applications = tokens
}
}
static func clearShield() {
AppRestrictShared.makeStore().clearAllSettings()
}
static func loadShieldImage() -> UIImage? {
let config = shieldConfig
switch config.imageSource {
case .album:
if let url = AppRestrictShared.customImageURL,
let data = try? Data(contentsOf: url),
let image = UIImage(data: data) {
return image
}
fallthrough
case .presetDefault:
return UIImage(named: "AppRestrict/shield_preset_default")
?? UIImage(systemName: "lock.shield.fill")
case .presetFocus:
return UIImage(named: "AppRestrict/shield_preset_focus")
?? UIImage(systemName: "brain.head.profile")
case .presetCalm:
return UIImage(named: "AppRestrict/shield_preset_calm")
?? UIImage(systemName: "leaf.fill")
}
}
static func saveCustomImage(_ image: UIImage) -> Bool {
guard let url = AppRestrictShared.customImageURL else { return false }
let resized = image.appRestrictResized(maxSide: 720)
guard let data = resized.jpegData(compressionQuality: 0.82) else { return false }
do {
try data.write(to: url, options: .atomic)
return true
} catch {
return false
}
}
}
private extension UIImage {
func appRestrictResized(maxSide: CGFloat) -> UIImage {
let maxCurrent = max(size.width, size.height)
guard maxCurrent > maxSide, maxCurrent > 0 else { return self }
let scale = maxSide / maxCurrent
let newSize = CGSize(width: size.width * scale, height: size.height * scale)
let renderer = UIGraphicsImageRenderer(size: newSize)
return renderer.image { _ in
draw(in: CGRect(origin: .zero, size: newSize))
}
}
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.family-controls</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.cn.zuomeng.jisuloca</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,27 @@
import DeviceActivity
import FamilyControls
import Foundation
import ManagedSettings
final class DeviceActivityMonitorExtension: DeviceActivityMonitor {
override func intervalDidStart(for activity: DeviceActivityName) {
super.intervalDidStart(for: activity)
let tokens = AppRestrictSharedStore.enabledTokens
AppRestrictSharedStore.applyShield(for: tokens)
}
override func intervalDidEnd(for activity: DeviceActivityName) {
super.intervalDidEnd(for: activity)
// Keep shields while user still has enabled tokens; only clear when empty.
let tokens = AppRestrictSharedStore.enabledTokens
if tokens.isEmpty {
AppRestrictSharedStore.clearShield()
} else {
AppRestrictSharedStore.applyShield(for: tokens)
}
}
override func eventDidReachThreshold(_ event: DeviceActivityEvent.Name, activity: DeviceActivityName) {
super.eventDidReachThreshold(event, activity: activity)
}
}

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>DeviceActivityMonitor</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.deviceactivity.monitor-extension</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).DeviceActivityMonitorExtension</string>
</dict>
</dict>
</plist>

View File

@ -1,6 +1,6 @@
# Uncomment the next line to define a global platform for your project # Uncomment the next line to define a global platform for your project
source 'https://gitee.com/mirrors/CocoaPods-Specs.git' source 'https://gitee.com/mirrors/CocoaPods-Specs.git'
platform :ios, '15.0' platform :ios, '16.0'
use_frameworks! use_frameworks!
target 'QuickLocation' do target 'QuickLocation' do
# Comment the next line if you don't want to use dynamic frameworks # Comment the next line if you don't want to use dynamic frameworks

View File

@ -7,6 +7,9 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
04532656C70B8C5AD6A8161E /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 226CA98495C2884EBB1D1372 /* Foundation.framework */; };
071EBEFAB7A70D8554B38024 /* DeviceActivity.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 88CC9D32980864EAD9F72004 /* DeviceActivity.framework */; };
091AC874363B91A91D717758 /* ManagedSettings.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2EE79029677394A46B143E75 /* ManagedSettings.framework */; };
305A76882FCA8C7000227D26 /* MoyaProvider+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74C62FCA8C7000227D26 /* MoyaProvider+Rx.swift */; }; 305A76882FCA8C7000227D26 /* MoyaProvider+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74C62FCA8C7000227D26 /* MoyaProvider+Rx.swift */; };
305A76892FCA8C7000227D26 /* Observable+Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74C72FCA8C7000227D26 /* Observable+Response.swift */; }; 305A76892FCA8C7000227D26 /* Observable+Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74C72FCA8C7000227D26 /* Observable+Response.swift */; };
305A768A2FCA8C7000227D26 /* Single+Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74C82FCA8C7000227D26 /* Single+Response.swift */; }; 305A768A2FCA8C7000227D26 /* Single+Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74C82FCA8C7000227D26 /* Single+Response.swift */; };
@ -47,7 +50,6 @@
305A76AD2FCA8C7000227D26 /* UIApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74F62FCA8C7000227D26 /* UIApplicationExtension.swift */; }; 305A76AD2FCA8C7000227D26 /* UIApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74F62FCA8C7000227D26 /* UIApplicationExtension.swift */; };
305A76AE2FCA8C7000227D26 /* UIButton+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74F72FCA8C7000227D26 /* UIButton+Extension.swift */; }; 305A76AE2FCA8C7000227D26 /* UIButton+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74F72FCA8C7000227D26 /* UIButton+Extension.swift */; };
305A76AF2FCA8C7000227D26 /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74F82FCA8C7000227D26 /* UIColor+Extension.swift */; }; 305A76AF2FCA8C7000227D26 /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74F82FCA8C7000227D26 /* UIColor+Extension.swift */; };
55B217A33022F0B100784706 /* UIDevice+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217A33022F0B100784707 /* UIDevice+Extension.swift */; };
305A76B02FCA8C7000227D26 /* UIFont+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74F92FCA8C7000227D26 /* UIFont+Extension.swift */; }; 305A76B02FCA8C7000227D26 /* UIFont+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74F92FCA8C7000227D26 /* UIFont+Extension.swift */; };
305A76B12FCA8C7000227D26 /* UIImage+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74FA2FCA8C7000227D26 /* UIImage+Extension.swift */; }; 305A76B12FCA8C7000227D26 /* UIImage+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74FA2FCA8C7000227D26 /* UIImage+Extension.swift */; };
305A76B22FCA8C7000227D26 /* UIImage+Resource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74FB2FCA8C7000227D26 /* UIImage+Resource.swift */; }; 305A76B22FCA8C7000227D26 /* UIImage+Resource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74FB2FCA8C7000227D26 /* UIImage+Resource.swift */; };
@ -187,11 +189,6 @@
30A87A642FEE75520095E7C6 /* CreateBubbleTipsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A632FEE75520095E7C6 /* CreateBubbleTipsView.swift */; }; 30A87A642FEE75520095E7C6 /* CreateBubbleTipsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A632FEE75520095E7C6 /* CreateBubbleTipsView.swift */; };
30A87A662FEE843E0095E7C6 /* CreateBubbleDoneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A652FEE843E0095E7C6 /* CreateBubbleDoneView.swift */; }; 30A87A662FEE843E0095E7C6 /* CreateBubbleDoneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A652FEE843E0095E7C6 /* CreateBubbleDoneView.swift */; };
30A87A682FEE86560095E7C6 /* CreateBubblePopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A672FEE86560095E7C6 /* CreateBubblePopView.swift */; }; 30A87A682FEE86560095E7C6 /* CreateBubblePopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A672FEE86560095E7C6 /* CreateBubblePopView.swift */; };
55B219C13024C00100784722 /* BubbleHeroView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219C13024C00100784721 /* BubbleHeroView.swift */; };
55B219C13024C00100784724 /* CreateBubbleSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219C13024C00100784723 /* CreateBubbleSetupView.swift */; };
55B219C13024C00100784726 /* BubbleKnowledgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219C13024C00100784725 /* BubbleKnowledgeView.swift */; };
55B219C13024C00100784728 /* BubbleKnowledgeVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219C13024C00100784727 /* BubbleKnowledgeVC.swift */; };
55C219C13024C00100784731 /* SearchLocationHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C219C13024C00100784730 /* SearchLocationHeaderView.swift */; };
30A87A6B2FEF5B950095E7C6 /* SearchLocationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A6A2FEF5B950095E7C6 /* SearchLocationView.swift */; }; 30A87A6B2FEF5B950095E7C6 /* SearchLocationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A6A2FEF5B950095E7C6 /* SearchLocationView.swift */; };
30A87A6D2FEF5BA10095E7C6 /* SearchLocationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A6C2FEF5BA10095E7C6 /* SearchLocationVC.swift */; }; 30A87A6D2FEF5BA10095E7C6 /* SearchLocationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A6C2FEF5BA10095E7C6 /* SearchLocationVC.swift */; };
30A87A6F2FEF7BE40095E7C6 /* SearchLocationResultVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A6E2FEF7BE40095E7C6 /* SearchLocationResultVC.swift */; }; 30A87A6F2FEF7BE40095E7C6 /* SearchLocationResultVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A6E2FEF7BE40095E7C6 /* SearchLocationResultVC.swift */; };
@ -239,8 +236,6 @@
30D74AAB2FE8C7700050EB2C /* GPSSignalHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AAA2FE8C7700050EB2C /* GPSSignalHelper.swift */; }; 30D74AAB2FE8C7700050EB2C /* GPSSignalHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AAA2FE8C7700050EB2C /* GPSSignalHelper.swift */; };
30D74AAE2FEA13E00050EB2C /* ScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AAD2FEA13E00050EB2C /* ScheduleView.swift */; }; 30D74AAE2FEA13E00050EB2C /* ScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AAD2FEA13E00050EB2C /* ScheduleView.swift */; };
30D74AB02FEA13ED0050EB2C /* ScheduleVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AAF2FEA13ED0050EB2C /* ScheduleVC.swift */; }; 30D74AB02FEA13ED0050EB2C /* ScheduleVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AAF2FEA13ED0050EB2C /* ScheduleVC.swift */; };
55B218B13024B00100784722 /* FeatureIntroVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218B13024B00100784721 /* FeatureIntroVC.swift */; };
55B218B13024B00100784724 /* FeatureIntroView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218B13024B00100784723 /* FeatureIntroView.swift */; };
30D74AB22FEA1D5D0050EB2C /* ScheduleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AB12FEA1D5D0050EB2C /* ScheduleViewModel.swift */; }; 30D74AB22FEA1D5D0050EB2C /* ScheduleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AB12FEA1D5D0050EB2C /* ScheduleViewModel.swift */; };
30D74AB42FEA25B90050EB2C /* ViewedModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AB32FEA25B90050EB2C /* ViewedModel.swift */; }; 30D74AB42FEA25B90050EB2C /* ViewedModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AB32FEA25B90050EB2C /* ViewedModel.swift */; };
30D74AB62FEA34FF0050EB2C /* ItineraryAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AB52FEA34FF0050EB2C /* ItineraryAPI.swift */; }; 30D74AB62FEA34FF0050EB2C /* ItineraryAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AB52FEA34FF0050EB2C /* ItineraryAPI.swift */; };
@ -300,11 +295,15 @@
30EFF3E52FDAA93400EB35D4 /* PrivacyPolicyVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF3E42FDAA93300EB35D4 /* PrivacyPolicyVC.swift */; }; 30EFF3E52FDAA93400EB35D4 /* PrivacyPolicyVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF3E42FDAA93300EB35D4 /* PrivacyPolicyVC.swift */; };
30EFF3E72FDAA93D00EB35D4 /* PrivacyPolicyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF3E62FDAA93D00EB35D4 /* PrivacyPolicyView.swift */; }; 30EFF3E72FDAA93D00EB35D4 /* PrivacyPolicyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF3E62FDAA93D00EB35D4 /* PrivacyPolicyView.swift */; };
30EFF3E82FCA8C7000227D26 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 30EFF3E92FCA8C7000227D26 /* AuthenticationServices.framework */; }; 30EFF3E82FCA8C7000227D26 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 30EFF3E92FCA8C7000227D26 /* AuthenticationServices.framework */; };
4392B1D9C125D873FCFCDDC9 /* app_catalog.json in Resources */ = {isa = PBXBuildFile; fileRef = 3C79865C3ED165AFDD091C29 /* app_catalog.json */; };
488FF21773B8E5021D8662B5 /* AppRestrictView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 787F87DD1CEBED1A4E1E9FC7 /* AppRestrictView.swift */; };
4AF6E5954D73E21F31F7A16B /* AppRestrictVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0112B7865829D5B6A4A0BD0 /* AppRestrictVC.swift */; };
557E8D5330187A5C0032AB51 /* BaseNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 557E8D5230187A5C0032AB51 /* BaseNavigationView.swift */; }; 557E8D5330187A5C0032AB51 /* BaseNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 557E8D5230187A5C0032AB51 /* BaseNavigationView.swift */; };
55B2179130217D6600784774 /* HomeView2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B2179030217D6600784774 /* HomeView2.swift */; }; 55B2179130217D6600784774 /* HomeView2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B2179030217D6600784774 /* HomeView2.swift */; };
55B2179330218CE100784774 /* GroupMemberView2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B2179230218CE100784774 /* GroupMemberView2.swift */; }; 55B2179330218CE100784774 /* GroupMemberView2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B2179230218CE100784774 /* GroupMemberView2.swift */; };
55B217953022DD6B00784774 /* 荆南波波黑-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 55B217943022DD6B00784774 /* 荆南波波黑-Bold.ttf */; }; 55B217953022DD6B00784774 /* 荆南波波黑-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 55B217943022DD6B00784774 /* 荆南波波黑-Bold.ttf */; };
55B217A13022F0A000784702 /* FontManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217A03022F0A000784701 /* FontManager.swift */; }; 55B217A13022F0A000784702 /* FontManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217A03022F0A000784701 /* FontManager.swift */; };
55B217A33022F0B100784706 /* UIDevice+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217A33022F0B100784707 /* UIDevice+Extension.swift */; };
55B217B13022F0A000784711 /* GroupItineraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217B03022F0A000784710 /* GroupItineraryView.swift */; }; 55B217B13022F0A000784711 /* GroupItineraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217B03022F0A000784710 /* GroupItineraryView.swift */; };
55B217C13022F0A000784721 /* MinePhotoWallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217C03022F0A000784720 /* MinePhotoWallView.swift */; }; 55B217C13022F0A000784721 /* MinePhotoWallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217C03022F0A000784720 /* MinePhotoWallView.swift */; };
55B217C33022F0A000784723 /* MemberPhoneReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217C23022F0A000784722 /* MemberPhoneReportView.swift */; }; 55B217C33022F0A000784723 /* MemberPhoneReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217C23022F0A000784722 /* MemberPhoneReportView.swift */; };
@ -322,14 +321,86 @@
55B218A13024A00100784708 /* CancelAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784707 /* CancelAccountView.swift */; }; 55B218A13024A00100784708 /* CancelAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784707 /* CancelAccountView.swift */; };
55B218A13024A0010078470A /* MemberInfoVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784709 /* MemberInfoVC.swift */; }; 55B218A13024A0010078470A /* MemberInfoVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784709 /* MemberInfoVC.swift */; };
55B218A13024A0010078470C /* MemberInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A0010078470B /* MemberInfoView.swift */; }; 55B218A13024A0010078470C /* MemberInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A0010078470B /* MemberInfoView.swift */; };
55B218B13024B00100784722 /* FeatureIntroVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218B13024B00100784721 /* FeatureIntroVC.swift */; };
55B218B13024B00100784724 /* FeatureIntroView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218B13024B00100784723 /* FeatureIntroView.swift */; };
55B219C13024C00100784722 /* BubbleHeroView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219C13024C00100784721 /* BubbleHeroView.swift */; };
55B219C13024C00100784724 /* CreateBubbleSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219C13024C00100784723 /* CreateBubbleSetupView.swift */; };
55B219C13024C00100784726 /* BubbleKnowledgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219C13024C00100784725 /* BubbleKnowledgeView.swift */; };
55B219C13024C00100784728 /* BubbleKnowledgeVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219C13024C00100784727 /* BubbleKnowledgeVC.swift */; };
55B21D6E3023117900784774 /* 优设标题黑_猫啃网.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 55B21D6D3023117900784774 /* 优设标题黑_猫啃网.ttf */; }; 55B21D6E3023117900784774 /* 优设标题黑_猫啃网.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 55B21D6D3023117900784774 /* 优设标题黑_猫啃网.ttf */; };
55BF752D2FFE53F70055DA57 /* LocationPermissionPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */; }; 55BF752D2FFE53F70055DA57 /* LocationPermissionPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */; };
55BF75352FFF91690055DA57 /* InteractionEmojiCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */; }; 55BF75352FFF91690055DA57 /* InteractionEmojiCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */; };
55C219C13024C00100784731 /* SearchLocationHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C219C13024C00100784730 /* SearchLocationHeaderView.swift */; };
60BEE25E61F3DE24976A7A47 /* ManagedSettingsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FE44DF1C46BDA7B98BC7859 /* ManagedSettingsUI.framework */; };
68F0D48EB76039050533197C /* AppRestrictShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AB29D0618C4BE76F2CA5261 /* AppRestrictShared.swift */; };
6D90E8FFE4D597E888E6A224 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E326B06736FE04DD7A5E96D /* UIKit.framework */; };
77731F107613C247B7FE98C8 /* ManagedSettings.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2EE79029677394A46B143E75 /* ManagedSettings.framework */; };
7B9D095A034C9FA5E69F55BA /* AppRestrictShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AB29D0618C4BE76F2CA5261 /* AppRestrictShared.swift */; };
80F749310651808BF00F95CF /* DeviceActivityMonitorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64156B4A0E806901AB223FA /* DeviceActivityMonitorExtension.swift */; };
830C70B1B53657BB020721DA /* AppRestrictManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEEA6CEA0CEAAB42C22752B /* AppRestrictManager.swift */; };
89647BC98E0FF342511D42CF /* FamilyControls.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CD6B965A124276661A84C4C /* FamilyControls.framework */; };
A1B2C3D42FDABC8C009215C1 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3D52FDABC8C009215C1 /* PrivacyInfo.xcprivacy */; }; A1B2C3D42FDABC8C009215C1 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3D52FDABC8C009215C1 /* PrivacyInfo.xcprivacy */; };
A42DCB2A66E694B7CC50033B /* AppRestrictCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89519661B70A0ADEC2A76032 /* AppRestrictCell.swift */; };
A55D8DE8A1F679EBF7B562D1 /* AppCatalogStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47922567FCD34DBE9186F8FD /* AppCatalogStore.swift */; };
A97A15CB71FDCA432974A768 /* AppRestrictShieldSettingsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFBDE92B94E7004F6B642AD /* AppRestrictShieldSettingsVC.swift */; };
B38B130801018F93E81D52C5 /* ManagedSettings.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2EE79029677394A46B143E75 /* ManagedSettings.framework */; };
B52F13A261ED273A3A58183F /* SelectActivityVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BAE595D39202A2958737BD3 /* SelectActivityVC.swift */; };
C15D41A1770F5B4A02E5FC59 /* AppRestrictShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AB29D0618C4BE76F2CA5261 /* AppRestrictShared.swift */; };
C3C4ACC1AE3CAF5BA21328B7 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 226CA98495C2884EBB1D1372 /* Foundation.framework */; };
C6A99DEE361AB31477F88475 /* LockDistractView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC46E80B848E4012D192E4CE /* LockDistractView.swift */; };
CC53DE2CA88E529129547A52 /* ShieldConfigurationExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 0C47E0012491DB1371FD5E53 /* ShieldConfigurationExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
D3C101B198090A9F4D93E44F /* FamilyControls.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CD6B965A124276661A84C4C /* FamilyControls.framework */; };
D60B79067C80CD6AF50D63FE /* FamilyControls.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CD6B965A124276661A84C4C /* FamilyControls.framework */; };
D698E9C56D6F2C152772131D /* Pods_QuickLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 475D33CAFA1E1911EB1F8D9F /* Pods_QuickLocation.framework */; }; D698E9C56D6F2C152772131D /* Pods_QuickLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 475D33CAFA1E1911EB1F8D9F /* Pods_QuickLocation.framework */; };
D765496DAEBAE6A8C98DB843 /* ShieldConfigurationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08002267FC4390B20D3510B3 /* ShieldConfigurationExtension.swift */; };
DB67C2EB2521042829A3DBA5 /* PairGuideStepsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E5F5DC8694A001BF0C47147 /* PairGuideStepsView.swift */; };
DBFE43A8D2406BF4EF92C2EF /* DeviceActivityMonitorExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 98E91E51811235EAD0985421 /* DeviceActivityMonitorExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
E3D4129BDA33540BA4E1292E /* LockDistractVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9CFFB14BA2ECC259151BCDA /* LockDistractVC.swift */; };
E6E5C711C9BD9F6C929BB062 /* DeviceActivity.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 88CC9D32980864EAD9F72004 /* DeviceActivity.framework */; };
E866CAAE7D4E74EB7AD608B9 /* ITunesSearchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE3D39F325FC7624C1D914D6 /* ITunesSearchService.swift */; };
EA13C1E21EC94958AC9755A8 /* FamilyActivityPickerHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF9D223AB35744F92E0E3E77 /* FamilyActivityPickerHost.swift */; };
F53E5BD11D671E5CED4E7408 /* ManagedSettingsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FE44DF1C46BDA7B98BC7859 /* ManagedSettingsUI.framework */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
1B096A922D2FECE87D42D1D4 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 3E4359002FC48D26003470A5 /* Project object */;
proxyType = 1;
remoteGlobalIDString = E4257D157F2E24904E9B6630;
remoteInfo = ShieldConfigurationExtension;
};
BCE404021C2F9BD8BCC26E94 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 3E4359002FC48D26003470A5 /* Project object */;
proxyType = 1;
remoteGlobalIDString = B626CA1007794E780B5B1CB3;
remoteInfo = DeviceActivityMonitorExtension;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
63D74ED0B1CEAFA8DAC0A6C0 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
DBFE43A8D2406BF4EF92C2EF /* DeviceActivityMonitorExtension.appex in Embed Foundation Extensions */,
CC53DE2CA88E529129547A52 /* ShieldConfigurationExtension.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
08002267FC4390B20D3510B3 /* ShieldConfigurationExtension.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ShieldConfigurationExtension.swift; path = ShieldConfigurationExtension/ShieldConfigurationExtension.swift; sourceTree = "<group>"; };
0C47E0012491DB1371FD5E53 /* ShieldConfigurationExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShieldConfigurationExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
1AB29D0618C4BE76F2CA5261 /* AppRestrictShared.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictShared.swift; path = AppRestrictShared/AppRestrictShared.swift; sourceTree = "<group>"; };
1E326B06736FE04DD7A5E96D /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
226CA98495C2884EBB1D1372 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; };
2EE79029677394A46B143E75 /* ManagedSettings.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ManagedSettings.framework; path = System/Library/Frameworks/ManagedSettings.framework; sourceTree = SDKROOT; };
305A74C62FCA8C7000227D26 /* MoyaProvider+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MoyaProvider+Rx.swift"; sourceTree = "<group>"; }; 305A74C62FCA8C7000227D26 /* MoyaProvider+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MoyaProvider+Rx.swift"; sourceTree = "<group>"; };
305A74C72FCA8C7000227D26 /* Observable+Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Observable+Response.swift"; sourceTree = "<group>"; }; 305A74C72FCA8C7000227D26 /* Observable+Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Observable+Response.swift"; sourceTree = "<group>"; };
305A74C82FCA8C7000227D26 /* Single+Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Single+Response.swift"; sourceTree = "<group>"; }; 305A74C82FCA8C7000227D26 /* Single+Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Single+Response.swift"; sourceTree = "<group>"; };
@ -372,7 +443,6 @@
305A74F62FCA8C7000227D26 /* UIApplicationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationExtension.swift; sourceTree = "<group>"; }; 305A74F62FCA8C7000227D26 /* UIApplicationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationExtension.swift; sourceTree = "<group>"; };
305A74F72FCA8C7000227D26 /* UIButton+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+Extension.swift"; sourceTree = "<group>"; }; 305A74F72FCA8C7000227D26 /* UIButton+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+Extension.swift"; sourceTree = "<group>"; };
305A74F82FCA8C7000227D26 /* UIColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extension.swift"; sourceTree = "<group>"; }; 305A74F82FCA8C7000227D26 /* UIColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extension.swift"; sourceTree = "<group>"; };
55B217A33022F0B100784707 /* UIDevice+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Extension.swift"; sourceTree = "<group>"; };
305A74F92FCA8C7000227D26 /* UIFont+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Extension.swift"; sourceTree = "<group>"; }; 305A74F92FCA8C7000227D26 /* UIFont+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Extension.swift"; sourceTree = "<group>"; };
305A74FA2FCA8C7000227D26 /* UIImage+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Extension.swift"; sourceTree = "<group>"; }; 305A74FA2FCA8C7000227D26 /* UIImage+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Extension.swift"; sourceTree = "<group>"; };
305A74FB2FCA8C7000227D26 /* UIImage+Resource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Resource.swift"; sourceTree = "<group>"; }; 305A74FB2FCA8C7000227D26 /* UIImage+Resource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Resource.swift"; sourceTree = "<group>"; };
@ -518,11 +588,6 @@
30A87A632FEE75520095E7C6 /* CreateBubbleTipsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleTipsView.swift; sourceTree = "<group>"; }; 30A87A632FEE75520095E7C6 /* CreateBubbleTipsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleTipsView.swift; sourceTree = "<group>"; };
30A87A652FEE843E0095E7C6 /* CreateBubbleDoneView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleDoneView.swift; sourceTree = "<group>"; }; 30A87A652FEE843E0095E7C6 /* CreateBubbleDoneView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleDoneView.swift; sourceTree = "<group>"; };
30A87A672FEE86560095E7C6 /* CreateBubblePopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubblePopView.swift; sourceTree = "<group>"; }; 30A87A672FEE86560095E7C6 /* CreateBubblePopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubblePopView.swift; sourceTree = "<group>"; };
55B219C13024C00100784721 /* BubbleHeroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleHeroView.swift; sourceTree = "<group>"; };
55B219C13024C00100784723 /* CreateBubbleSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleSetupView.swift; sourceTree = "<group>"; };
55B219C13024C00100784725 /* BubbleKnowledgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleKnowledgeView.swift; sourceTree = "<group>"; };
55B219C13024C00100784727 /* BubbleKnowledgeVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleKnowledgeVC.swift; sourceTree = "<group>"; };
55C219C13024C00100784730 /* SearchLocationHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationHeaderView.swift; sourceTree = "<group>"; };
30A87A6A2FEF5B950095E7C6 /* SearchLocationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationView.swift; sourceTree = "<group>"; }; 30A87A6A2FEF5B950095E7C6 /* SearchLocationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationView.swift; sourceTree = "<group>"; };
30A87A6C2FEF5BA10095E7C6 /* SearchLocationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationVC.swift; sourceTree = "<group>"; }; 30A87A6C2FEF5BA10095E7C6 /* SearchLocationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationVC.swift; sourceTree = "<group>"; };
30A87A6E2FEF7BE40095E7C6 /* SearchLocationResultVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationResultVC.swift; sourceTree = "<group>"; }; 30A87A6E2FEF7BE40095E7C6 /* SearchLocationResultVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationResultVC.swift; sourceTree = "<group>"; };
@ -571,8 +636,6 @@
30D74AAA2FE8C7700050EB2C /* GPSSignalHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GPSSignalHelper.swift; sourceTree = "<group>"; }; 30D74AAA2FE8C7700050EB2C /* GPSSignalHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GPSSignalHelper.swift; sourceTree = "<group>"; };
30D74AAD2FEA13E00050EB2C /* ScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleView.swift; sourceTree = "<group>"; }; 30D74AAD2FEA13E00050EB2C /* ScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleView.swift; sourceTree = "<group>"; };
30D74AAF2FEA13ED0050EB2C /* ScheduleVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleVC.swift; sourceTree = "<group>"; }; 30D74AAF2FEA13ED0050EB2C /* ScheduleVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleVC.swift; sourceTree = "<group>"; };
55B218B13024B00100784721 /* FeatureIntroVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIntroVC.swift; sourceTree = "<group>"; };
55B218B13024B00100784723 /* FeatureIntroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIntroView.swift; sourceTree = "<group>"; };
30D74AB12FEA1D5D0050EB2C /* ScheduleViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleViewModel.swift; sourceTree = "<group>"; }; 30D74AB12FEA1D5D0050EB2C /* ScheduleViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleViewModel.swift; sourceTree = "<group>"; };
30D74AB32FEA25B90050EB2C /* ViewedModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewedModel.swift; sourceTree = "<group>"; }; 30D74AB32FEA25B90050EB2C /* ViewedModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewedModel.swift; sourceTree = "<group>"; };
30D74AB52FEA34FF0050EB2C /* ItineraryAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItineraryAPI.swift; sourceTree = "<group>"; }; 30D74AB52FEA34FF0050EB2C /* ItineraryAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItineraryAPI.swift; sourceTree = "<group>"; };
@ -634,13 +697,17 @@
30EFF3E42FDAA93300EB35D4 /* PrivacyPolicyVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyPolicyVC.swift; sourceTree = "<group>"; }; 30EFF3E42FDAA93300EB35D4 /* PrivacyPolicyVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyPolicyVC.swift; sourceTree = "<group>"; };
30EFF3E62FDAA93D00EB35D4 /* PrivacyPolicyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyPolicyView.swift; sourceTree = "<group>"; }; 30EFF3E62FDAA93D00EB35D4 /* PrivacyPolicyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyPolicyView.swift; sourceTree = "<group>"; };
30EFF3E92FCA8C7000227D26 /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = System/Library/Frameworks/AuthenticationServices.framework; sourceTree = SDKROOT; }; 30EFF3E92FCA8C7000227D26 /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = System/Library/Frameworks/AuthenticationServices.framework; sourceTree = SDKROOT; };
3C79865C3ED165AFDD091C29 /* app_catalog.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = app_catalog.json; path = AppRestrict/app_catalog.json; sourceTree = "<group>"; };
3E4359082FC48D26003470A5 /* QuickLocation.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QuickLocation.app; sourceTree = BUILT_PRODUCTS_DIR; }; 3E4359082FC48D26003470A5 /* QuickLocation.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QuickLocation.app; sourceTree = BUILT_PRODUCTS_DIR; };
475D33CAFA1E1911EB1F8D9F /* Pods_QuickLocation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_QuickLocation.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 475D33CAFA1E1911EB1F8D9F /* Pods_QuickLocation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_QuickLocation.framework; sourceTree = BUILT_PRODUCTS_DIR; };
47922567FCD34DBE9186F8FD /* AppCatalogStore.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppCatalogStore.swift; path = AppRestrict/AppCatalogStore.swift; sourceTree = "<group>"; };
4CD6B965A124276661A84C4C /* FamilyControls.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FamilyControls.framework; path = System/Library/Frameworks/FamilyControls.framework; sourceTree = SDKROOT; };
557E8D5230187A5C0032AB51 /* BaseNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseNavigationView.swift; sourceTree = "<group>"; }; 557E8D5230187A5C0032AB51 /* BaseNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseNavigationView.swift; sourceTree = "<group>"; };
55B2179030217D6600784774 /* HomeView2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView2.swift; sourceTree = "<group>"; }; 55B2179030217D6600784774 /* HomeView2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView2.swift; sourceTree = "<group>"; };
55B2179230218CE100784774 /* GroupMemberView2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberView2.swift; sourceTree = "<group>"; }; 55B2179230218CE100784774 /* GroupMemberView2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberView2.swift; sourceTree = "<group>"; };
55B217943022DD6B00784774 /* 荆南波波黑-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "荆南波波黑-Bold.ttf"; sourceTree = "<group>"; }; 55B217943022DD6B00784774 /* 荆南波波黑-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "荆南波波黑-Bold.ttf"; sourceTree = "<group>"; };
55B217A03022F0A000784701 /* FontManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontManager.swift; sourceTree = "<group>"; }; 55B217A03022F0A000784701 /* FontManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontManager.swift; sourceTree = "<group>"; };
55B217A33022F0B100784707 /* UIDevice+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Extension.swift"; sourceTree = "<group>"; };
55B217B03022F0A000784710 /* GroupItineraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupItineraryView.swift; sourceTree = "<group>"; }; 55B217B03022F0A000784710 /* GroupItineraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupItineraryView.swift; sourceTree = "<group>"; };
55B217C03022F0A000784720 /* MinePhotoWallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MinePhotoWallView.swift; sourceTree = "<group>"; }; 55B217C03022F0A000784720 /* MinePhotoWallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MinePhotoWallView.swift; sourceTree = "<group>"; };
55B217C23022F0A000784722 /* MemberPhoneReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberPhoneReportView.swift; sourceTree = "<group>"; }; 55B217C23022F0A000784722 /* MemberPhoneReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberPhoneReportView.swift; sourceTree = "<group>"; };
@ -658,32 +725,62 @@
55B218A13024A00100784707 /* CancelAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelAccountView.swift; sourceTree = "<group>"; }; 55B218A13024A00100784707 /* CancelAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelAccountView.swift; sourceTree = "<group>"; };
55B218A13024A00100784709 /* MemberInfoVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberInfoVC.swift; sourceTree = "<group>"; }; 55B218A13024A00100784709 /* MemberInfoVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberInfoVC.swift; sourceTree = "<group>"; };
55B218A13024A0010078470B /* MemberInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberInfoView.swift; sourceTree = "<group>"; }; 55B218A13024A0010078470B /* MemberInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberInfoView.swift; sourceTree = "<group>"; };
55B218B13024B00100784721 /* FeatureIntroVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIntroVC.swift; sourceTree = "<group>"; };
55B218B13024B00100784723 /* FeatureIntroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIntroView.swift; sourceTree = "<group>"; };
55B219C13024C00100784721 /* BubbleHeroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleHeroView.swift; sourceTree = "<group>"; };
55B219C13024C00100784723 /* CreateBubbleSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleSetupView.swift; sourceTree = "<group>"; };
55B219C13024C00100784725 /* BubbleKnowledgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleKnowledgeView.swift; sourceTree = "<group>"; };
55B219C13024C00100784727 /* BubbleKnowledgeVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleKnowledgeVC.swift; sourceTree = "<group>"; };
55B21D6D3023117900784774 /* 优设标题黑_猫啃网.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "优设标题黑_猫啃网.ttf"; sourceTree = "<group>"; }; 55B21D6D3023117900784774 /* 优设标题黑_猫啃网.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "优设标题黑_猫啃网.ttf"; sourceTree = "<group>"; };
55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissionPopView.swift; sourceTree = "<group>"; }; 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissionPopView.swift; sourceTree = "<group>"; };
55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionEmojiCell.swift; sourceTree = "<group>"; }; 55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionEmojiCell.swift; sourceTree = "<group>"; };
55C219C13024C00100784730 /* SearchLocationHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationHeaderView.swift; sourceTree = "<group>"; };
5E5F5DC8694A001BF0C47147 /* PairGuideStepsView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PairGuideStepsView.swift; path = AppRestrict/PairGuideStepsView.swift; sourceTree = "<group>"; };
6BAE595D39202A2958737BD3 /* SelectActivityVC.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SelectActivityVC.swift; path = AppRestrict/SelectActivityVC.swift; sourceTree = "<group>"; };
787F87DD1CEBED1A4E1E9FC7 /* AppRestrictView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictView.swift; path = AppRestrict/AppRestrictView.swift; sourceTree = "<group>"; };
7AEEA6CEA0CEAAB42C22752B /* AppRestrictManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictManager.swift; path = AppRestrict/AppRestrictManager.swift; sourceTree = "<group>"; };
7FE44DF1C46BDA7B98BC7859 /* ManagedSettingsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ManagedSettingsUI.framework; path = System/Library/Frameworks/ManagedSettingsUI.framework; sourceTree = SDKROOT; };
88CC9D32980864EAD9F72004 /* DeviceActivity.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DeviceActivity.framework; path = System/Library/Frameworks/DeviceActivity.framework; sourceTree = SDKROOT; };
89519661B70A0ADEC2A76032 /* AppRestrictCell.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictCell.swift; path = AppRestrict/AppRestrictCell.swift; sourceTree = "<group>"; };
98E91E51811235EAD0985421 /* DeviceActivityMonitorExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = DeviceActivityMonitorExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
A1B2C3D52FDABC8C009215C1 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; }; A1B2C3D52FDABC8C009215C1 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
AC46E80B848E4012D192E4CE /* LockDistractView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LockDistractView.swift; sourceTree = "<group>"; };
BE3D39F325FC7624C1D914D6 /* ITunesSearchService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ITunesSearchService.swift; path = AppRestrict/ITunesSearchService.swift; sourceTree = "<group>"; };
BFFBDE92B94E7004F6B642AD /* AppRestrictShieldSettingsVC.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictShieldSettingsVC.swift; path = AppRestrict/AppRestrictShieldSettingsVC.swift; sourceTree = "<group>"; };
D0112B7865829D5B6A4A0BD0 /* AppRestrictVC.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictVC.swift; path = AppRestrict/AppRestrictVC.swift; sourceTree = "<group>"; };
D64156B4A0E806901AB223FA /* DeviceActivityMonitorExtension.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DeviceActivityMonitorExtension.swift; path = DeviceActivityMonitorExtension/DeviceActivityMonitorExtension.swift; sourceTree = "<group>"; };
DA16D49AA46D4F6838340B55 /* Pods-QuickLocation.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-QuickLocation.debug.xcconfig"; path = "Target Support Files/Pods-QuickLocation/Pods-QuickLocation.debug.xcconfig"; sourceTree = "<group>"; }; DA16D49AA46D4F6838340B55 /* Pods-QuickLocation.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-QuickLocation.debug.xcconfig"; path = "Target Support Files/Pods-QuickLocation/Pods-QuickLocation.debug.xcconfig"; sourceTree = "<group>"; };
E483B9929C03809ADEDE8341 /* Pods-QuickLocation.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-QuickLocation.release.xcconfig"; path = "Target Support Files/Pods-QuickLocation/Pods-QuickLocation.release.xcconfig"; sourceTree = "<group>"; }; E483B9929C03809ADEDE8341 /* Pods-QuickLocation.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-QuickLocation.release.xcconfig"; path = "Target Support Files/Pods-QuickLocation/Pods-QuickLocation.release.xcconfig"; sourceTree = "<group>"; };
EF9D223AB35744F92E0E3E77 /* FamilyActivityPickerHost.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FamilyActivityPickerHost.swift; path = AppRestrict/FamilyActivityPickerHost.swift; sourceTree = "<group>"; };
F9CFFB14BA2ECC259151BCDA /* LockDistractVC.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LockDistractVC.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFileSystemSynchronizedRootGroup section */
30CCDF8E2FE3E63B00F5214A /* sound */ = { 30CCDF8E2FE3E63B00F5214A /* sound */ = {
isa = PBXFileSystemSynchronizedRootGroup; isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = sound; path = sound;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
30CCDF902FE3E63B00F5214A /* video */ = { 30CCDF902FE3E63B00F5214A /* video */ = {
isa = PBXFileSystemSynchronizedRootGroup; isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = video; path = video;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
30CCE01E2FE3E64700F5214A /* lotties */ = { 30CCE01E2FE3E64700F5214A /* lotties */ = {
isa = PBXFileSystemSynchronizedRootGroup; isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = lotties; path = lotties;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
30D87CEF2FDFF52100E958FD /* TTGTagCollectionView */ = { 30D87CEF2FDFF52100E958FD /* TTGTagCollectionView */ = {
isa = PBXFileSystemSynchronizedRootGroup; isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = TTGTagCollectionView; path = TTGTagCollectionView;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@ -696,12 +793,63 @@
files = ( files = (
30EFF3E82FCA8C7000227D26 /* AuthenticationServices.framework in Frameworks */, 30EFF3E82FCA8C7000227D26 /* AuthenticationServices.framework in Frameworks */,
D698E9C56D6F2C152772131D /* Pods_QuickLocation.framework in Frameworks */, D698E9C56D6F2C152772131D /* Pods_QuickLocation.framework in Frameworks */,
89647BC98E0FF342511D42CF /* FamilyControls.framework in Frameworks */,
77731F107613C247B7FE98C8 /* ManagedSettings.framework in Frameworks */,
E6E5C711C9BD9F6C929BB062 /* DeviceActivity.framework in Frameworks */,
60BEE25E61F3DE24976A7A47 /* ManagedSettingsUI.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
57D97F1D073CB957F5B3FC99 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
C3C4ACC1AE3CAF5BA21328B7 /* Foundation.framework in Frameworks */,
071EBEFAB7A70D8554B38024 /* DeviceActivity.framework in Frameworks */,
D60B79067C80CD6AF50D63FE /* FamilyControls.framework in Frameworks */,
091AC874363B91A91D717758 /* ManagedSettings.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
6314CDE3DFF338A21DC2A612 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
04532656C70B8C5AD6A8161E /* Foundation.framework in Frameworks */,
B38B130801018F93E81D52C5 /* ManagedSettings.framework in Frameworks */,
F53E5BD11D671E5CED4E7408 /* ManagedSettingsUI.framework in Frameworks */,
D3C101B198090A9F4D93E44F /* FamilyControls.framework in Frameworks */,
6D90E8FFE4D597E888E6A224 /* UIKit.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
2234314E8DD57630628CC5C3 /* ShieldConfigurationExtension */ = {
isa = PBXGroup;
children = (
08002267FC4390B20D3510B3 /* ShieldConfigurationExtension.swift */,
);
name = ShieldConfigurationExtension;
sourceTree = "<group>";
};
2509CBAFBD63A140B359BD7F /* DeviceActivityMonitorExtension */ = {
isa = PBXGroup;
children = (
D64156B4A0E806901AB223FA /* DeviceActivityMonitorExtension.swift */,
);
name = DeviceActivityMonitorExtension;
sourceTree = "<group>";
};
2F518AB6587CC227CC515D8C /* AppRestrict */ = {
isa = PBXGroup;
children = (
7AEEA6CEA0CEAAB42C22752B /* AppRestrictManager.swift */,
);
name = AppRestrict;
sourceTree = "<group>";
};
305A74C92FCA8C7000227D26 /* RxMoya */ = { 305A74C92FCA8C7000227D26 /* RxMoya */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -981,6 +1129,7 @@
305A752C2FCA8C7000227D26 /* URL */, 305A752C2FCA8C7000227D26 /* URL */,
30D87CD52FDF9F1900E958FD /* MQTT */, 30D87CD52FDF9F1900E958FD /* MQTT */,
30C6666A2FFB7C3000E62B25 /* IAPManager */, 30C6666A2FFB7C3000E62B25 /* IAPManager */,
2F518AB6587CC227CC515D8C /* AppRestrict */,
); );
path = Manager; path = Manager;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1156,6 +1305,8 @@
30ACD54A2FF6332200174861 /* PopupWindow */, 30ACD54A2FF6332200174861 /* PopupWindow */,
30EFF3AD2FD7FF1400EB35D4 /* TextInput */, 30EFF3AD2FD7FF1400EB35D4 /* TextInput */,
30C6679D2FFB7FEF00E62B25 /* Share */, 30C6679D2FFB7FEF00E62B25 /* Share */,
F49062649CA8E5F9FA2F22A5 /* AppRestrict */,
B79BE06D49443FE82A811D5C /* LockDistract */,
); );
path = Section; path = Section;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1564,15 +1715,6 @@
path = SOS; path = SOS;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
55B218B13024B00100784720 /* Explore */ = {
isa = PBXGroup;
children = (
55B218B13024B00100784721 /* FeatureIntroVC.swift */,
55B218B13024B00100784723 /* FeatureIntroView.swift */,
);
path = Explore;
sourceTree = "<group>";
};
30D74AAC2FEA13BD0050EB2C /* Schedule */ = { 30D74AAC2FEA13BD0050EB2C /* Schedule */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -1728,6 +1870,9 @@
3E4359092FC48D26003470A5 /* Products */, 3E4359092FC48D26003470A5 /* Products */,
B07D45692FCECE07570D9B73 /* Pods */, B07D45692FCECE07570D9B73 /* Pods */,
47CD8471BE2146A2656CF27E /* Frameworks */, 47CD8471BE2146A2656CF27E /* Frameworks */,
DA2257EC3F3167F4A6AFE759 /* AppRestrictShared */,
2509CBAFBD63A140B359BD7F /* DeviceActivityMonitorExtension */,
2234314E8DD57630628CC5C3 /* ShieldConfigurationExtension */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@ -1735,6 +1880,8 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
3E4359082FC48D26003470A5 /* QuickLocation.app */, 3E4359082FC48D26003470A5 /* QuickLocation.app */,
98E91E51811235EAD0985421 /* DeviceActivityMonitorExtension.appex */,
0C47E0012491DB1371FD5E53 /* ShieldConfigurationExtension.appex */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1744,6 +1891,12 @@
children = ( children = (
30EFF3E92FCA8C7000227D26 /* AuthenticationServices.framework */, 30EFF3E92FCA8C7000227D26 /* AuthenticationServices.framework */,
475D33CAFA1E1911EB1F8D9F /* Pods_QuickLocation.framework */, 475D33CAFA1E1911EB1F8D9F /* Pods_QuickLocation.framework */,
4CD6B965A124276661A84C4C /* FamilyControls.framework */,
2EE79029677394A46B143E75 /* ManagedSettings.framework */,
88CC9D32980864EAD9F72004 /* DeviceActivity.framework */,
7FE44DF1C46BDA7B98BC7859 /* ManagedSettingsUI.framework */,
74B9CD2ABDBC4C8CE3107CDE /* iOS */,
1E326B06736FE04DD7A5E96D /* UIKit.framework */,
); );
name = Frameworks; name = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1774,6 +1927,16 @@
path = TodayTrackDetail; path = TodayTrackDetail;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
55B217DE302301000078473D /* PhoneReportDetail */ = {
isa = PBXGroup;
children = (
55B217D73023010000784736 /* PhoneReportDetailVC.swift */,
55B217D93023010000784738 /* PhoneReportDetailView.swift */,
55B217DB302301000078473A /* PhoneReportDetailViewModel.swift */,
);
path = PhoneReportDetail;
sourceTree = "<group>";
};
55B218A13024A00100784710 /* About */ = { 55B218A13024A00100784710 /* About */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -1801,14 +1964,21 @@
path = MemberInfo; path = MemberInfo;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
55B217DE302301000078473D /* PhoneReportDetail */ = { 55B218B13024B00100784720 /* Explore */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
55B217D73023010000784736 /* PhoneReportDetailVC.swift */, 55B218B13024B00100784721 /* FeatureIntroVC.swift */,
55B217D93023010000784738 /* PhoneReportDetailView.swift */, 55B218B13024B00100784723 /* FeatureIntroView.swift */,
55B217DB302301000078473A /* PhoneReportDetailViewModel.swift */,
); );
path = PhoneReportDetail; path = Explore;
sourceTree = "<group>";
};
74B9CD2ABDBC4C8CE3107CDE /* iOS */ = {
isa = PBXGroup;
children = (
226CA98495C2884EBB1D1372 /* Foundation.framework */,
);
name = iOS;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
B07D45692FCECE07570D9B73 /* Pods */ = { B07D45692FCECE07570D9B73 /* Pods */ = {
@ -1820,6 +1990,40 @@
path = Pods; path = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
B79BE06D49443FE82A811D5C /* LockDistract */ = {
isa = PBXGroup;
children = (
F9CFFB14BA2ECC259151BCDA /* LockDistractVC.swift */,
AC46E80B848E4012D192E4CE /* LockDistractView.swift */,
);
path = LockDistract;
sourceTree = "<group>";
};
DA2257EC3F3167F4A6AFE759 /* AppRestrictShared */ = {
isa = PBXGroup;
children = (
1AB29D0618C4BE76F2CA5261 /* AppRestrictShared.swift */,
);
name = AppRestrictShared;
sourceTree = "<group>";
};
F49062649CA8E5F9FA2F22A5 /* AppRestrict */ = {
isa = PBXGroup;
children = (
47922567FCD34DBE9186F8FD /* AppCatalogStore.swift */,
89519661B70A0ADEC2A76032 /* AppRestrictCell.swift */,
D0112B7865829D5B6A4A0BD0 /* AppRestrictVC.swift */,
787F87DD1CEBED1A4E1E9FC7 /* AppRestrictView.swift */,
BFFBDE92B94E7004F6B642AD /* AppRestrictShieldSettingsVC.swift */,
EF9D223AB35744F92E0E3E77 /* FamilyActivityPickerHost.swift */,
BE3D39F325FC7624C1D914D6 /* ITunesSearchService.swift */,
6BAE595D39202A2958737BD3 /* SelectActivityVC.swift */,
3C79865C3ED165AFDD091C29 /* app_catalog.json */,
5E5F5DC8694A001BF0C47147 /* PairGuideStepsView.swift */,
);
name = AppRestrict;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@ -1833,10 +2037,13 @@
3E4359062FC48D26003470A5 /* Resources */, 3E4359062FC48D26003470A5 /* Resources */,
22E76BAEDC74B6177770F922 /* [CP] Embed Pods Frameworks */, 22E76BAEDC74B6177770F922 /* [CP] Embed Pods Frameworks */,
84E518A2C7E1AE699B07AC0D /* [CP] Copy Pods Resources */, 84E518A2C7E1AE699B07AC0D /* [CP] Copy Pods Resources */,
63D74ED0B1CEAFA8DAC0A6C0 /* Embed Foundation Extensions */,
); );
buildRules = ( buildRules = (
); );
dependencies = ( dependencies = (
0950A334CDF61EB8C1C94506 /* PBXTargetDependency */,
DCCA06707CFDC3C0D07BD8B0 /* PBXTargetDependency */,
); );
fileSystemSynchronizedGroups = ( fileSystemSynchronizedGroups = (
30CCDF8E2FE3E63B00F5214A /* sound */, 30CCDF8E2FE3E63B00F5214A /* sound */,
@ -1849,6 +2056,40 @@
productReference = 3E4359082FC48D26003470A5 /* QuickLocation.app */; productReference = 3E4359082FC48D26003470A5 /* QuickLocation.app */;
productType = "com.apple.product-type.application"; productType = "com.apple.product-type.application";
}; };
B626CA1007794E780B5B1CB3 /* DeviceActivityMonitorExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = 1A6BE372491582A933CD3ECE /* Build configuration list for PBXNativeTarget "DeviceActivityMonitorExtension" */;
buildPhases = (
933B6388A2F481EE1DF7A99C /* Sources */,
57D97F1D073CB957F5B3FC99 /* Frameworks */,
7FA03AF172854DD407AFED91 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = DeviceActivityMonitorExtension;
productName = DeviceActivityMonitorExtension;
productReference = 98E91E51811235EAD0985421 /* DeviceActivityMonitorExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
E4257D157F2E24904E9B6630 /* ShieldConfigurationExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = BAA60EF75453DD3B5A4775AF /* Build configuration list for PBXNativeTarget "ShieldConfigurationExtension" */;
buildPhases = (
EF5DDEE45DD2FBD2EA2AAB8D /* Sources */,
6314CDE3DFF338A21DC2A612 /* Frameworks */,
6E86F27A156DD5A139D94E1B /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = ShieldConfigurationExtension;
productName = ShieldConfigurationExtension;
productReference = 0C47E0012491DB1371FD5E53 /* ShieldConfigurationExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
/* End PBXNativeTarget section */ /* End PBXNativeTarget section */
/* Begin PBXProject section */ /* Begin PBXProject section */
@ -1879,6 +2120,8 @@
projectRoot = ""; projectRoot = "";
targets = ( targets = (
3E4359072FC48D26003470A5 /* QuickLocation */, 3E4359072FC48D26003470A5 /* QuickLocation */,
B626CA1007794E780B5B1CB3 /* DeviceActivityMonitorExtension */,
E4257D157F2E24904E9B6630 /* ShieldConfigurationExtension */,
); );
}; };
/* End PBXProject section */ /* End PBXProject section */
@ -1896,6 +2139,21 @@
55B217953022DD6B00784774 /* 荆南波波黑-Bold.ttf in Resources */, 55B217953022DD6B00784774 /* 荆南波波黑-Bold.ttf in Resources */,
55B21D6E3023117900784774 /* 优设标题黑_猫啃网.ttf in Resources */, 55B21D6E3023117900784774 /* 优设标题黑_猫啃网.ttf in Resources */,
305A77FC2FCA8C7000227D26 /* Main.storyboard in Resources */, 305A77FC2FCA8C7000227D26 /* Main.storyboard in Resources */,
4392B1D9C125D873FCFCDDC9 /* app_catalog.json in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
6E86F27A156DD5A139D94E1B /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
7FA03AF172854DD407AFED91 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -2283,11 +2541,57 @@
305A771B2FCA8C7000227D26 /* ReusableView.swift in Sources */, 305A771B2FCA8C7000227D26 /* ReusableView.swift in Sources */,
305A771C2FCA8C7000227D26 /* AppDelegate.swift in Sources */, 305A771C2FCA8C7000227D26 /* AppDelegate.swift in Sources */,
305A771D2FCA8C7000227D26 /* ViewController.swift in Sources */, 305A771D2FCA8C7000227D26 /* ViewController.swift in Sources */,
C15D41A1770F5B4A02E5FC59 /* AppRestrictShared.swift in Sources */,
830C70B1B53657BB020721DA /* AppRestrictManager.swift in Sources */,
A55D8DE8A1F679EBF7B562D1 /* AppCatalogStore.swift in Sources */,
A42DCB2A66E694B7CC50033B /* AppRestrictCell.swift in Sources */,
4AF6E5954D73E21F31F7A16B /* AppRestrictVC.swift in Sources */,
488FF21773B8E5021D8662B5 /* AppRestrictView.swift in Sources */,
A97A15CB71FDCA432974A768 /* AppRestrictShieldSettingsVC.swift in Sources */,
EA13C1E21EC94958AC9755A8 /* FamilyActivityPickerHost.swift in Sources */,
B52F13A261ED273A3A58183F /* SelectActivityVC.swift in Sources */,
C6A99DEE361AB31477F88475 /* LockDistractView.swift in Sources */,
E3D4129BDA33540BA4E1292E /* LockDistractVC.swift in Sources */,
E866CAAE7D4E74EB7AD608B9 /* ITunesSearchService.swift in Sources */,
DB67C2EB2521042829A3DBA5 /* PairGuideStepsView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
933B6388A2F481EE1DF7A99C /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
80F749310651808BF00F95CF /* DeviceActivityMonitorExtension.swift in Sources */,
68F0D48EB76039050533197C /* AppRestrictShared.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
EF5DDEE45DD2FBD2EA2AAB8D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D765496DAEBAE6A8C98DB843 /* ShieldConfigurationExtension.swift in Sources */,
7B9D095A034C9FA5E69F55BA /* AppRestrictShared.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
/* End PBXSourcesBuildPhase section */ /* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
0950A334CDF61EB8C1C94506 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
name = DeviceActivityMonitorExtension;
target = B626CA1007794E780B5B1CB3 /* DeviceActivityMonitorExtension */;
targetProxy = BCE404021C2F9BD8BCC26E94 /* PBXContainerItemProxy */;
};
DCCA06707CFDC3C0D07BD8B0 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
name = ShieldConfigurationExtension;
target = E4257D157F2E24904E9B6630 /* ShieldConfigurationExtension */;
targetProxy = 1B096A922D2FECE87D42D1D4 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */ /* Begin PBXVariantGroup section */
305A76822FCA8C7000227D26 /* LaunchScreen.storyboard */ = { 305A76822FCA8C7000227D26 /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup; isa = PBXVariantGroup;
@ -2310,6 +2614,61 @@
/* End PBXVariantGroup section */ /* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */ /* Begin XCBuildConfiguration section */
15B6023456261CCD0E3A673E /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_OBJC_WEAK = NO;
CODE_SIGN_ENTITLEMENTS = DeviceActivityMonitorExtension/DeviceActivityMonitorExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = LRDLWZ2Y83;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = DeviceActivityMonitorExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = cn.zuomeng.jisuloca.DeviceActivityMonitor;
PRODUCT_NAME = DeviceActivityMonitorExtension;
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = nonisolated;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
3A68FBB9A55555685B490D5C /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_OBJC_WEAK = NO;
CODE_SIGN_ENTITLEMENTS = DeviceActivityMonitorExtension/DeviceActivityMonitorExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = LRDLWZ2Y83;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = DeviceActivityMonitorExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = cn.zuomeng.jisuloca.DeviceActivityMonitor;
PRODUCT_NAME = DeviceActivityMonitorExtension;
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = nonisolated;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
};
3E43591C2FC48D2B003470A5 /* Debug */ = { 3E43591C2FC48D2B003470A5 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = DA16D49AA46D4F6838340B55 /* Pods-QuickLocation.debug.xcconfig */; baseConfigurationReference = DA16D49AA46D4F6838340B55 /* Pods-QuickLocation.debug.xcconfig */;
@ -2339,7 +2698,7 @@
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UIMainStoryboardFile = Main;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
IPHONEOS_DEPLOYMENT_TARGET = 15; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -2393,7 +2752,7 @@
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UIMainStoryboardFile = Main;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
IPHONEOS_DEPLOYMENT_TARGET = 15; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -2471,7 +2830,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.6; IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
@ -2529,7 +2888,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.6; IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
@ -2539,9 +2898,73 @@
}; };
name = Release; name = Release;
}; };
A0AD067561B0A604B5DF5049 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_OBJC_WEAK = NO;
CODE_SIGN_ENTITLEMENTS = ShieldConfigurationExtension/ShieldConfigurationExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = LRDLWZ2Y83;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = ShieldConfigurationExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = cn.zuomeng.jisuloca.ShieldConfiguration;
PRODUCT_NAME = ShieldConfigurationExtension;
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = nonisolated;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
};
B66BC4F86D908B8200A8DC8E /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_OBJC_WEAK = NO;
CODE_SIGN_ENTITLEMENTS = ShieldConfigurationExtension/ShieldConfigurationExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = LRDLWZ2Y83;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = ShieldConfigurationExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = cn.zuomeng.jisuloca.ShieldConfiguration;
PRODUCT_NAME = ShieldConfigurationExtension;
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = nonisolated;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
/* End XCBuildConfiguration section */ /* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */ /* Begin XCConfigurationList section */
1A6BE372491582A933CD3ECE /* Build configuration list for PBXNativeTarget "DeviceActivityMonitorExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
15B6023456261CCD0E3A673E /* Release */,
3A68FBB9A55555685B490D5C /* Debug */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
3E4359032FC48D26003470A5 /* Build configuration list for PBXProject "QuickLocation" */ = { 3E4359032FC48D26003470A5 /* Build configuration list for PBXProject "QuickLocation" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
@ -2560,6 +2983,15 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
BAA60EF75453DD3B5A4775AF /* Build configuration list for PBXNativeTarget "ShieldConfigurationExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
B66BC4F86D908B8200A8DC8E /* Release */,
A0AD067561B0A604B5DF5049 /* Debug */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */ /* End XCConfigurationList section */
}; };
rootObject = 3E4359002FC48D26003470A5 /* Project object */; rootObject = 3E4359002FC48D26003470A5 /* Project object */;

View File

@ -0,0 +1,6 @@
{
"info" : { "author" : "xcode", "version" : 1 },
"properties" : {
"provides-namespace" : true
}
}

View File

@ -0,0 +1,8 @@
{
"images" : [
{ "idiom" : "universal", "scale" : "1x" },
{ "filename" : "pair_action_delete@2x.png", "idiom" : "universal", "scale" : "2x" },
{ "filename" : "pair_action_delete@3x.png", "idiom" : "universal", "scale" : "3x" }
],
"info" : { "author" : "xcode", "version" : 1 }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 867 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,8 @@
{
"images" : [
{ "idiom" : "universal", "scale" : "1x" },
{ "filename" : "pair_action_link@2x.png", "idiom" : "universal", "scale" : "2x" },
{ "filename" : "pair_action_link@3x.png", "idiom" : "universal", "scale" : "3x" }
],
"info" : { "author" : "xcode", "version" : 1 }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,8 @@
{
"images" : [
{ "idiom" : "universal", "scale" : "1x" },
{ "filename" : "pair_badge_check@2x.png", "idiom" : "universal", "scale" : "2x" },
{ "filename" : "pair_badge_check@3x.png", "idiom" : "universal", "scale" : "3x" }
],
"info" : { "author" : "xcode", "version" : 1 }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,8 @@
{
"images" : [
{ "idiom" : "universal", "scale" : "1x" },
{ "filename" : "pair_link_active@2x.png", "idiom" : "universal", "scale" : "2x" },
{ "filename" : "pair_link_active@3x.png", "idiom" : "universal", "scale" : "3x" }
],
"info" : { "author" : "xcode", "version" : 1 }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -0,0 +1,8 @@
{
"images" : [
{ "idiom" : "universal", "scale" : "1x" },
{ "filename" : "pair_link_inactive@2x.png", "idiom" : "universal", "scale" : "2x" },
{ "filename" : "pair_link_inactive@3x.png", "idiom" : "universal", "scale" : "3x" }
],
"info" : { "author" : "xcode", "version" : 1 }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 678 B

After

Width:  |  Height:  |  Size: 830 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 869 B

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "join_hero_bg@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "join_hero_bg@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "join_scan@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "join_scan@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -0,0 +1,8 @@
{
"images" : [
{ "idiom" : "universal", "scale" : "1x" },
{ "filename" : "app_unknown@2x.png", "idiom" : "universal", "scale" : "2x" },
{ "filename" : "app_unknown@3x.png", "idiom" : "universal", "scale" : "3x" }
],
"info" : { "author" : "xcode", "version" : 1 }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,8 @@
{
"images" : [
{ "idiom" : "universal", "scale" : "1x" },
{ "filename" : "arrow_right@2x.png", "idiom" : "universal", "scale" : "2x" },
{ "filename" : "arrow_right@3x.png", "idiom" : "universal", "scale" : "3x" }
],
"info" : { "author" : "xcode", "version" : 1 }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 B

View File

@ -0,0 +1,8 @@
{
"images" : [
{ "idiom" : "universal", "scale" : "1x" },
{ "filename" : "section_star@2x.png", "idiom" : "universal", "scale" : "2x" },
{ "filename" : "section_star@3x.png", "idiom" : "universal", "scale" : "3x" }
],
"info" : { "author" : "xcode", "version" : 1 }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 772 B

View File

@ -0,0 +1,8 @@
{
"images" : [
{ "idiom" : "universal", "scale" : "1x" },
{ "filename" : "wallpaper_1@2x.png", "idiom" : "universal", "scale" : "2x" },
{ "filename" : "wallpaper_1@3x.png", "idiom" : "universal", "scale" : "3x" }
],
"info" : { "author" : "xcode", "version" : 1 }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@ -0,0 +1,8 @@
{
"images" : [
{ "idiom" : "universal", "scale" : "1x" },
{ "filename" : "wallpaper_2@2x.png", "idiom" : "universal", "scale" : "2x" },
{ "filename" : "wallpaper_2@3x.png", "idiom" : "universal", "scale" : "3x" }
],
"info" : { "author" : "xcode", "version" : 1 }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

@ -0,0 +1,8 @@
{
"images" : [
{ "idiom" : "universal", "scale" : "1x" },
{ "filename" : "wallpaper_3@2x.png", "idiom" : "universal", "scale" : "2x" },
{ "filename" : "wallpaper_3@3x.png", "idiom" : "universal", "scale" : "3x" }
],
"info" : { "author" : "xcode", "version" : 1 }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@ -44,7 +44,7 @@ class BaseViewController: UIViewController {
// Do any additional setup after loading the view. // Do any additional setup after loading the view.
fd_prefersNavigationBarHidden = isNavigationBarHidden fd_prefersNavigationBarHidden = isNavigationBarHidden
view.backgroundColor = .white//ThemeManager.shared.color.backgroundColor //view.backgroundColor = .white//ThemeManager.shared.color.backgroundColor
// setupNavigationBar() // setupNavigationBar()
setupLeftItem() setupLeftItem()
} }

View File

@ -85,6 +85,10 @@ enum Route: String {
case memberInfo = "memberInfo" case memberInfo = "memberInfo"
/// ///
case cancelAccount = "cancelAccount" case cancelAccount = "cancelAccount"
/// App
case appRestrict = "appRestrict"
///
case lockDistract = "lockDistract"
/// / /// /
case signIn = "signIn" case signIn = "signIn"
/// SOS /// SOS
@ -388,6 +392,18 @@ extension AppRouter: AppRouterProtocol {
CancelAccountVC() CancelAccountVC()
} }
// MARK: - App
AppRouter.register(Route.appRestrict) { _, _ in
AppRestrictFactory.make()
}
// MARK: -
AppRouter.register(Route.lockDistract) { _, _ in
let vc = LockDistractVC()
vc.isNeedLogin = true
return vc
}
// MARK: - // MARK: -
AppRouter.register(Route.signIn) { _, _ in AppRouter.register(Route.signIn) { _, _ in
SignInVC(lastLocation: nil) SignInVC(lastLocation: nil)

View File

@ -0,0 +1,140 @@
//
// AppRestrictManager.swift
// QuickLocation
//
import DeviceActivity
import FamilyControls
import Foundation
import ManagedSettings
import UIKit
@available(iOS 16.0, *)
final class AppRestrictManager {
static let shared = AppRestrictManager()
private let center = AuthorizationCenter.shared
private let activityCenter = DeviceActivityCenter()
private let activityName = DeviceActivityName(AppRestrictShared.activityName)
private init() {}
var authorizationStatus: AuthorizationStatus {
center.authorizationStatus
}
var isAuthorized: Bool {
center.authorizationStatus == .approved
}
var selection: FamilyActivitySelection {
get { AppRestrictSharedStore.selection }
set {
AppRestrictSharedStore.selection = newValue
// Drop enabled tokens that are no longer in selection
let apps = newValue.applicationTokens
AppRestrictSharedStore.enabledTokens = AppRestrictSharedStore.enabledTokens.intersection(apps)
refreshMonitoringAndShield()
}
}
var applicationTokens: [ApplicationToken] {
Array(selection.applicationTokens)
}
var enabledTokens: Set<ApplicationToken> {
get { AppRestrictSharedStore.enabledTokens }
set {
AppRestrictSharedStore.enabledTokens = newValue
refreshMonitoringAndShield()
}
}
func requestAuthorization() async throws {
try await center.requestAuthorization(for: .individual)
}
func mergeSelection(_ incoming: FamilyActivitySelection) {
var current = selection
current.applicationTokens.formUnion(incoming.applicationTokens)
current.categoryTokens.formUnion(incoming.categoryTokens)
current.webDomainTokens.formUnion(incoming.webDomainTokens)
selection = current
}
func isEnabled(_ token: ApplicationToken) -> Bool {
enabledTokens.contains(token)
}
func setEnabled(_ token: ApplicationToken, enabled: Bool) {
var set = enabledTokens
if enabled {
set.insert(token)
} else {
set.remove(token)
}
enabledTokens = set
}
func catalogId(for token: ApplicationToken) -> String? {
AppRestrictSharedStore.catalogId(for: token)
}
func link(catalogId: String, token: ApplicationToken, displayName: String? = nil, iconURL: String? = nil) {
AppRestrictSharedStore.setLink(
catalogId: catalogId,
token: token,
displayName: displayName,
iconURL: iconURL
)
}
func catalogItem(for token: ApplicationToken) -> AppCatalogItem? {
guard let record = AppRestrictSharedStore.linkRecord(for: token) else { return nil }
return AppCatalogStore.resolve(link: record)
}
func unlink(_ token: ApplicationToken) {
AppRestrictSharedStore.removeLink(for: token)
}
func removeApplication(_ token: ApplicationToken) {
unlink(token)
var set = enabledTokens
set.remove(token)
enabledTokens = set
var current = selection
current.applicationTokens.remove(token)
selection = current
}
var shieldConfig: AppRestrictShieldConfig {
get { AppRestrictSharedStore.shieldConfig }
set { AppRestrictSharedStore.shieldConfig = newValue }
}
@discardableResult
func saveCustomShieldImage(_ image: UIImage) -> Bool {
AppRestrictSharedStore.saveCustomImage(image)
}
func refreshMonitoringAndShield() {
let tokens = enabledTokens
AppRestrictSharedStore.applyShield(for: tokens)
activityCenter.stopMonitoring([activityName])
guard !tokens.isEmpty else { return }
// Near-daily schedule so Monitor can re-apply after reboot / schedule boundaries.
let schedule = DeviceActivitySchedule(
intervalStart: DateComponents(hour: 0, minute: 0),
intervalEnd: DateComponents(hour: 23, minute: 59),
repeats: true
)
do {
try activityCenter.startMonitoring(activityName, during: schedule)
} catch {
// Shield already applied above; monitoring is best-effort.
print("[AppRestrict] startMonitoring failed: \(error)")
}
}
}

View File

@ -12,5 +12,11 @@
</array> </array>
<key>com.apple.developer.networking.wifi-info</key> <key>com.apple.developer.networking.wifi-info</key>
<true/> <true/>
<key>com.apple.developer.family-controls</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.cn.zuomeng.jisuloca</string>
</array>
</dict> </dict>
</plist> </plist>

View File

@ -0,0 +1,112 @@
//
// AppCatalogStore.swift
// QuickLocation
//
import Foundation
import UIKit
import Kingfisher
struct AppCatalogItem: Codable, Equatable {
let id: String
let name: String
let icon: String
let keywords: [String]
let iconURL: String?
init(id: String, name: String, icon: String, keywords: [String], iconURL: String? = nil) {
self.id = id
self.name = name
self.icon = icon
self.keywords = keywords
self.iconURL = iconURL
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
icon = try container.decodeIfPresent(String.self, forKey: .icon) ?? ""
keywords = try container.decodeIfPresent([String].self, forKey: .keywords) ?? []
iconURL = try container.decodeIfPresent(String.self, forKey: .iconURL)
}
var isRemoteIcon: Bool {
guard let iconURL, !iconURL.isEmpty else { return false }
return icon.isEmpty
}
}
enum AppCatalogStore {
static let sharedItems: [AppCatalogItem] = {
guard let url = Bundle.main.url(forResource: "app_catalog", withExtension: "json"),
let data = try? Data(contentsOf: url),
let items = try? JSONDecoder().decode([AppCatalogItem].self, from: data) else {
return []
}
return items
}()
static func item(id: String) -> AppCatalogItem? {
sharedItems.first { $0.id == id }
}
static func resolve(link: AppRestrictLinkRecord) -> AppCatalogItem? {
if let local = item(id: link.catalogId) {
return local
}
guard let name = link.displayName, !name.isEmpty else { return nil }
return AppCatalogItem(
id: link.catalogId,
name: name,
icon: "",
keywords: [],
iconURL: link.iconURL
)
}
static func search(_ query: String) -> [AppCatalogItem] {
let q = query.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
guard !q.isEmpty else { return sharedItems }
return sharedItems.filter { item in
item.name.lowercased().contains(q)
|| item.keywords.contains(where: { $0.lowercased().contains(q) })
}
}
static func image(for item: AppCatalogItem) -> UIImage? {
guard !item.icon.isEmpty else { return placeholder(for: item.name) }
return UIImage(named: item.icon) ?? placeholder(for: item.name)
}
static func setIcon(for imageView: UIImageView, item: AppCatalogItem, cornerRadius: CGFloat = 10) {
imageView.layer.cornerRadius = cornerRadius
imageView.clipsToBounds = true
imageView.contentMode = .scaleAspectFill
if let urlString = item.iconURL, let url = URL(string: urlString) {
imageView.kf.setImage(with: url, placeholder: placeholder(for: item.name))
} else {
imageView.kf.cancelDownloadTask()
imageView.image = image(for: item)
}
}
private static func placeholder(for name: String) -> UIImage? {
let size = CGSize(width: 44, height: 44)
let renderer = UIGraphicsImageRenderer(size: size)
return renderer.image { _ in
UIColor(hexStr: "#16B3FF").setFill()
UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 10).fill()
let letter = String(name.prefix(1)) as NSString
let attrs: [NSAttributedString.Key: Any] = [
.font: UIFont.systemFont(ofSize: 18, weight: .bold),
.foregroundColor: UIColor.white
]
let textSize = letter.size(withAttributes: attrs)
letter.draw(
at: CGPoint(x: (size.width - textSize.width) / 2, y: (size.height - textSize.height) / 2),
withAttributes: attrs
)
}
}
}

View File

@ -0,0 +1,164 @@
//
// AppRestrictCell.swift
// QuickLocation
//
import FamilyControls
import UIKit
import ManagedSettings
@available(iOS 16.0, *)
final class AppRestrictCell: UITableViewCell {
static let reuseId = "AppRestrictCell"
private enum Metric {
static let appIcon: CGFloat = 40
static let linkIcon: CGFloat = 28
static let actionWidth: CGFloat = 48
static let actionHeight: CGFloat = 56
/// divider 8 + 48 + 8 + 48 + 12
static let actionAreaWidth: CGFloat = 124
}
var onPair: (() -> Void)?
var onDelete: (() -> Void)?
private let systemIconHost = UIView()
private let linkIconView = UIImageView()
private let catalogIconView = UIImageView()
private let placeholderView = UIView()
private let placeholderLab = UILabel()
private let divider = UIView()
private let pairBtn = UIButton(type: .custom)
private let deleteBtn = UIButton(type: .custom)
private var systemTokenHost: ApplicationTokenIconHostingView?
override init(style: CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
contentView.backgroundColor = .clear
setup()
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
private func setup() {
contentView.addSubview(systemIconHost)
contentView.addSubview(linkIconView)
contentView.addSubview(catalogIconView)
contentView.addSubview(placeholderView)
contentView.addSubview(divider)
contentView.addSubview(pairBtn)
contentView.addSubview(deleteBtn)
systemIconHost.clipsToBounds = true
systemIconHost.layer.cornerRadius = 10
systemIconHost.layoutChain
.left(16)
.centerY()
.width(Metric.appIcon)
.height(Metric.appIcon)
linkIconView.contentMode = .scaleAspectFit
linkIconView.layoutChain
.leftToRightOfView(systemIconHost, offset: 12)
.centerY()
.width(Metric.linkIcon)
.height(Metric.linkIcon)
catalogIconView.contentMode = .scaleAspectFill
catalogIconView.layer.cornerRadius = 10
catalogIconView.clipsToBounds = true
catalogIconView.layoutChain
.leftToRightOfView(linkIconView, offset: 12)
.centerY()
.width(Metric.appIcon)
.height(Metric.appIcon)
placeholderView.backgroundColor = UIColor(hexStr: "#F3F4F6")
placeholderView.layer.cornerRadius = Metric.appIcon / 2
placeholderView.layer.borderWidth = 1
placeholderView.layer.borderColor = UIColor(hexStr: "#E5E7EB").cgColor
placeholderView.layoutChain
.leftToRightOfView(linkIconView, offset: 12)
.centerY()
.width(Metric.appIcon)
.height(Metric.appIcon)
placeholderView.addSubview(placeholderLab)
placeholderLab.text = "?"
placeholderLab.font = .systemFont(ofSize: 18, weight: .bold)
placeholderLab.textColor = UIColor(hexStr: "#9CA3AF")
placeholderLab.textAlignment = .center
placeholderLab.layoutChain.center()
divider.backgroundColor = UIColor(hexStr: "#F0F0F0")
divider.layoutChain
.right(Metric.actionAreaWidth)
.width(1)
.top(12)
.bottom(12)
configureActionButton(pairBtn, imageName: "AppRestrict/pair_action_link", title: "配对", color: UIColor(hexStr: "#00ADFE"))
configureActionButton(deleteBtn, imageName: "AppRestrict/pair_action_delete", title: "删除", color: UIColor(hexStr: "#FF2323"))
pairBtn.addTarget(self, action: #selector(tapPair), for: .touchUpInside)
deleteBtn.addTarget(self, action: #selector(tapDelete), for: .touchUpInside)
pairBtn.layoutChain
.leftToRightOfView(divider, offset: 8)
.centerY()
.width(Metric.actionWidth)
.height(Metric.actionHeight)
deleteBtn.layoutChain
.leftToRightOfView(pairBtn, offset: 8)
.centerY()
.width(Metric.actionWidth)
.height(Metric.actionHeight)
}
override func layoutSubviews() {
super.layoutSubviews()
pairBtn.setImageEdge(.top, spacing: 4)
deleteBtn.setImageEdge(.top, spacing: 4)
}
private func configureActionButton(_ button: UIButton, imageName: String, title: String, color: UIColor) {
button.setImage(UIImage(named: imageName)?.withRenderingMode(.alwaysOriginal), for: .normal)
button.setTitle(title, for: .normal)
button.setTitleColor(color, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
button.titleLabel?.textAlignment = .center
button.titleLabel?.lineBreakMode = .byClipping
}
func configure(token: ApplicationToken, catalog: AppCatalogItem?) {
systemTokenHost?.subviews.forEach { $0.removeFromSuperview() }
let host = ApplicationTokenIconHostingView(token: token)
systemIconHost.addSubview(host)
host.layoutChain.edges()
systemTokenHost = host
let isPaired = catalog != nil
linkIconView.image = UIImage(named: isPaired ? "AppRestrict/pair_link_active" : "AppRestrict/pair_link_inactive")?
.withRenderingMode(.alwaysOriginal)
catalogIconView.isHidden = !isPaired
placeholderView.isHidden = isPaired
if let catalog {
AppCatalogStore.setIcon(for: catalogIconView, item: catalog, cornerRadius: 10)
} else {
catalogIconView.image = nil
}
}
@objc private func tapPair() {
onPair?()
}
@objc private func tapDelete() {
onDelete?()
}
}

View File

@ -0,0 +1,147 @@
//
// AppRestrictShieldSettingsVC.swift
// QuickLocation
//
import PhotosUI
import UIKit
@available(iOS 16.0, *)
final class AppRestrictShieldSettingsVC: BaseViewController {
override var isNavigationBarHidden: Bool { false }
private let titleField = UITextField()
private let subtitleField = UITextField()
private let buttonField = UITextField()
private let preview = UIImageView()
private var config = AppRestrictManager.shared.shieldConfig
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(hexStr: "#FAFAFA")
title = "锁定界面设置"
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "保存", style: .done, target: self, action: #selector(save))
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 12
view.addSubview(stack)
stack.layoutChain.top(kNaviHeight + 20).left(16).right(16)
func makeField(_ placeholder: String, text: String) -> UITextField {
let f = UITextField()
f.placeholder = placeholder
f.text = text
f.borderStyle = .roundedRect
f.font = .systemFont(ofSize: 15)
f.backgroundColor = .white
return f
}
titleField.text = config.title
subtitleField.text = config.subtitle
buttonField.text = config.primaryButtonLabel
[titleField, subtitleField, buttonField].forEach {
$0.borderStyle = .roundedRect
$0.font = .systemFont(ofSize: 15)
$0.backgroundColor = .white
stack.addArrangedSubview($0)
$0.layoutChain.height(44)
}
titleField.placeholder = "标题"
subtitleField.placeholder = "副标题"
buttonField.placeholder = "按钮文案"
let presetLab = UILabel()
presetLab.text = "预设图集"
presetLab.font = .systemFont(ofSize: 14, weight: .bold)
stack.addArrangedSubview(presetLab)
let presetRow = UIStackView()
presetRow.axis = .horizontal
presetRow.spacing = 10
presetRow.distribution = .fillEqually
[("默认", AppRestrictShieldConfig.ImageSource.presetDefault),
("专注", .presetFocus),
("平静", .presetCalm)].forEach { title, source in
let btn = UIButton(type: .system)
btn.setTitle(title, for: .normal)
btn.backgroundColor = .white
btn.layer.cornerRadius = 8
btn.tag = source.hashValue
btn.addAction(UIAction { [weak self] _ in
self?.config.imageSource = source
self?.refreshPreview()
}, for: .touchUpInside)
presetRow.addArrangedSubview(btn)
btn.layoutChain.height(40)
}
stack.addArrangedSubview(presetRow)
let albumBtn = UIButton(type: .system)
albumBtn.setTitle("从相册选择", for: .normal)
albumBtn.backgroundColor = UIColor(hexStr: "#16B3FF")
albumBtn.setTitleColor(.white, for: .normal)
albumBtn.layer.cornerRadius = 10
albumBtn.addTarget(self, action: #selector(pickAlbum), for: .touchUpInside)
stack.addArrangedSubview(albumBtn)
albumBtn.layoutChain.height(44)
preview.contentMode = .scaleAspectFit
preview.backgroundColor = .white
preview.layer.cornerRadius = 12
preview.clipsToBounds = true
stack.addArrangedSubview(preview)
preview.layoutChain.height(180)
refreshPreview()
}
private func refreshPreview() {
preview.image = AppRestrictSharedStore.loadShieldImage()
}
@objc private func save() {
config.title = titleField.text?.trimmingCharacters(in: .whitespacesAndNewlines).nilIfEmpty ?? AppRestrictShieldConfig.default.title
config.subtitle = subtitleField.text?.trimmingCharacters(in: .whitespacesAndNewlines).nilIfEmpty ?? AppRestrictShieldConfig.default.subtitle
config.primaryButtonLabel = buttonField.text?.trimmingCharacters(in: .whitespacesAndNewlines).nilIfEmpty ?? AppRestrictShieldConfig.default.primaryButtonLabel
AppRestrictManager.shared.shieldConfig = config
DLToast.showSuccess(text: "已保存")
navigationController?.popViewController(animated: true)
}
@objc private func pickAlbum() {
var conf = PHPickerConfiguration(photoLibrary: .shared())
conf.filter = .images
conf.selectionLimit = 1
let picker = PHPickerViewController(configuration: conf)
picker.delegate = self
present(picker, animated: true)
}
}
@available(iOS 16.0, *)
extension AppRestrictShieldSettingsVC: PHPickerViewControllerDelegate {
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
guard let provider = results.first?.itemProvider,
provider.canLoadObject(ofClass: UIImage.self) else { return }
provider.loadObject(ofClass: UIImage.self) { [weak self] object, _ in
guard let image = object as? UIImage else { return }
DispatchQueue.main.async {
guard let self else { return }
if AppRestrictManager.shared.saveCustomShieldImage(image) {
self.config.imageSource = .album
self.refreshPreview()
} else {
DLToast.show(text: "图片保存失败")
}
}
}
}
}
private extension String {
var nilIfEmpty: String? { isEmpty ? nil : self }
}

View File

@ -0,0 +1,141 @@
//
// AppRestrictVC.swift
// QuickLocation
//
import FamilyControls
import UIKit
import RxSwift
import RxCocoa
import ManagedSettings
@available(iOS 16.0, *)
final class AppRestrictVC: BaseViewController {
private var rootView: AppRestrictView!
private var tokens: [ApplicationToken] = []
private var linkingToken: ApplicationToken?
override func loadView() {
rootView = AppRestrictView(frame: UIScreen.main.bounds)
view = rootView
}
override func viewDidLoad() {
super.viewDidLoad()
rootView.tableView.dataSource = self
rootView.tableView.delegate = self
rootView.tableView.register(AppRestrictCell.self, forCellReuseIdentifier: AppRestrictCell.reuseId)
bind()
reload()
}
private func bind() {
rootView.navView.onBackTapped = { [weak self] in
self?.navigationController?.popViewController(animated: true)
}
rootView.addBtn.rx.tap.subscribe(onNext: { [weak self] in
self?.addApps()
}).disposed(by: disposeBag)
}
private func reload() {
tokens = AppRestrictManager.shared.applicationTokens
rootView.setListVisible(!tokens.isEmpty, rowCount: tokens.count)
rootView.tableView.reloadData()
}
private func addApps() {
Task { @MainActor in
do {
if !AppRestrictManager.shared.isAuthorized {
try await AppRestrictManager.shared.requestAuthorization()
}
guard AppRestrictManager.shared.isAuthorized else {
DLToast.show(text: "需要屏幕使用时间权限")
return
}
let presenter = FamilyActivityPickerPresenter(selection: AppRestrictManager.shared.selection)
presenter.onComplete = { [weak self] selection in
AppRestrictManager.shared.mergeSelection(selection)
self?.reload()
}
present(presenter, animated: false)
} catch {
DLToast.show(text: "授权失败,请在设置中开启屏幕使用时间权限")
}
}
}
private func openLink(for token: ApplicationToken) {
linkingToken = token
let vc = SelectActivityVC()
vc.onSelect = { [weak self] item in
guard let self, let token = self.linkingToken else { return }
AppRestrictManager.shared.link(
catalogId: item.id,
token: token,
displayName: item.name,
iconURL: item.iconURL
)
self.linkingToken = nil
self.reload()
}
let nav = UINavigationController(rootViewController: vc)
present(nav, animated: true)
}
private func deleteApp(_ token: ApplicationToken) {
AppRestrictManager.shared.removeApplication(token)
reload()
}
}
@available(iOS 16.0, *)
extension AppRestrictVC: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
tokens.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: AppRestrictCell.reuseId, for: indexPath) as! AppRestrictCell
let token = tokens[indexPath.row]
let catalog = AppRestrictManager.shared.catalogItem(for: token)
cell.configure(token: token, catalog: catalog)
cell.onPair = { [weak self] in
self?.openLink(for: token)
}
cell.onDelete = { [weak self] in
self?.deleteApp(token)
}
return cell
}
}
/// Fallback when system is below iOS 16.
final class AppRestrictUnsupportedVC: BaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
let nav = BaseNavigationView(title: "应用配对库")
view.addSubview(nav)
nav.layoutChain.top().edgesHorzontal().height(kNaviHeight)
nav.onBackTapped = { [weak self] in
self?.navigationController?.popViewController(animated: true)
}
let lab = UILabel()
lab.text = "该功能需要 iOS 16 及以上系统"
lab.textAlignment = .center
lab.textColor = UIColor(hexStr: "#767676")
view.addSubview(lab)
lab.layoutChain.center()
}
}
enum AppRestrictFactory {
static func make() -> UIViewController {
if #available(iOS 16.0, *) {
return AppRestrictVC()
}
return AppRestrictUnsupportedVC()
}
}

View File

@ -0,0 +1,188 @@
//
// AppRestrictView.swift
// QuickLocation
//
import UIKit
final class AppRestrictView: UIView {
let navView = BaseNavigationView(title: "应用配对库")
let scrollView = UIScrollView()
let contentView = UIView()
let guideCard = UIView()
let exampleCard = UIView()
let listCard = UIView()
let tableView = UITableView(frame: .zero, style: .plain)
let addBtn = UIButton(type: .custom)
private var tableHeightConstraint: NSLayoutConstraint?
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor(hexStr: "#FAFAFA")
setup()
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
func setListVisible(_ visible: Bool, rowCount: Int, rowHeight: CGFloat = 80) {
listCard.isHidden = !visible
tableHeightConstraint?.constant = visible ? rowHeight * CGFloat(rowCount) : 0
layoutIfNeeded()
}
private func setup() {
addSubview(navView)
addSubview(scrollView)
addSubview(addBtn)
scrollView.addSubview(contentView)
contentView.addSubview(guideCard)
contentView.addSubview(exampleCard)
contentView.addSubview(listCard)
listCard.addSubview(tableView)
navView.layoutChain.top().edgesHorzontal().height(kNaviHeight)
addBtn.setTitle("新增配对应用", for: .normal)
addBtn.setTitleColor(.white, for: .normal)
addBtn.titleLabel?.font = FontManager.boboBold(18)
addBtn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
addBtn.cornerRadius = 20
addBtn.layoutChain
.left(16)
.right(16)
.bottom(kSafeBottomMargin + 16)
.height(56)
scrollView.layoutChain
.topToBottomOfView(navView)
.edgesHorzontal()
.bottomToTopOfView(addBtn, offset: -12)
contentView.layoutChain
.edges()
.widthToView(scrollView)
guideCard.layoutChain
.top(12)
.edgesHorzontal(16)
exampleCard.layoutChain
.topToBottomOfView(guideCard, offset: 12)
.edgesHorzontal(16)
listCard.layoutChain
.topToBottomOfView(exampleCard, offset: 12)
.edgesHorzontal(16)
.bottom(12)
tableView.backgroundColor = .clear
tableView.separatorStyle = .none
tableView.isScrollEnabled = false
tableView.rowHeight = 80
tableView.layoutChain.edges(all: 12)
tableHeightConstraint = tableView.heightAnchor.constraint(equalToConstant: 0)
tableHeightConstraint?.isActive = true
styleCard(guideCard)
styleCard(exampleCard)
styleCard(listCard)
setupGuideCard()
setupExampleCard()
}
private func styleCard(_ card: UIView) {
card.backgroundColor = .white
card.layer.cornerRadius = 26
card.clipsToBounds = true
}
private func setupGuideCard() {
let titleLab = UILabel()
let attr = NSMutableAttributedString(
string: "如何配对?只需 ",
attributes: [
.font: UIFont.systemFont(ofSize: 14, weight: .bold),
.foregroundColor: UIColor(hexStr: "#293445")
]
)
attr.append(NSAttributedString(
string: "3",
attributes: [
.font: FontManager.boboBold(16),
.foregroundColor: UIColor(hexStr: "#00ADFE")
]
))
attr.append(NSAttributedString(
string: "",
attributes: [
.font: UIFont.systemFont(ofSize: 14, weight: .bold),
.foregroundColor: UIColor(hexStr: "#293445")
]
))
titleLab.attributedText = attr
guideCard.addSubview(titleLab)
titleLab.layoutChain.top(20).left(20).right(20)
let stepsView = PairGuideStepsView()
guideCard.addSubview(stepsView)
stepsView.layoutChain
.topToBottomOfView(titleLab, offset: 20)
.edgesHorzontal(20)
.bottom(20)
.height(72)
}
private func setupExampleCard() {
let badge = UIView()
badge.backgroundColor = UIColor(hexStr: "#F1FFED")
badge.layer.cornerRadius = 8
badge.clipsToBounds = true
exampleCard.addSubview(badge)
badge.layoutChain.top(10).left(10)
let checkIcon = UIImageView(
image: UIImage(named: "AppRestrict/pair_badge_check")?.withRenderingMode(.alwaysOriginal)
)
checkIcon.contentMode = .scaleAspectFit
let badgeLab = UILabel()
badgeLab.text = "正确示例"
badgeLab.font = .systemFont(ofSize: 12, weight: .medium)
badgeLab.textColor = UIColor(hexStr: "#00BB47")
badge.addSubview(checkIcon)
checkIcon.layoutChain.width(12).height(12).edgesVertical(7).left(4)
badge.addSubview(badgeLab)
badgeLab.layoutChain.leftToRightOfView(checkIcon, offset: 1).right(5).centerY()
let wechatItem = AppCatalogStore.item(id: "wechat")
let leftIcon = UIImageView()
let rightIcon = UIImageView()
let linkIcon = UIImageView(image: UIImage(named: "AppRestrict/pair_link_active")?.withRenderingMode(.alwaysOriginal))
[leftIcon, rightIcon].forEach {
$0.contentMode = .scaleAspectFill
$0.layer.cornerRadius = 10
$0.clipsToBounds = true
}
if let wechatItem {
AppCatalogStore.setIcon(for: leftIcon, item: wechatItem)
AppCatalogStore.setIcon(for: rightIcon, item: wechatItem)
}
linkIcon.contentMode = .scaleAspectFit
let row = UIStackView(arrangedSubviews: [leftIcon, linkIcon, rightIcon])
row.axis = .horizontal
row.alignment = .center
row.spacing = 12
exampleCard.addSubview(row)
row.layoutChain
.top(36)
.centerX()
.bottom(14)
leftIcon.layoutChain.width(50).height(50)
rightIcon.layoutChain.width(50).height(50)
linkIcon.layoutChain.width(35).height(25)
}
}

View File

@ -0,0 +1,135 @@
//
// FamilyActivityPickerHost.swift
// QuickLocation
//
import FamilyControls
import SwiftUI
import UIKit
import ManagedSettings
@available(iOS 16.0, *)
private struct FamilyActivityPickerSheet: View {
@State private var selection: FamilyActivitySelection
@State private var isPresented = true
let onComplete: (FamilyActivitySelection) -> Void
init(initial: FamilyActivitySelection, onComplete: @escaping (FamilyActivitySelection) -> Void) {
_selection = State(initialValue: initial)
self.onComplete = onComplete
}
var body: some View {
Color.clear
.familyActivityPicker(isPresented: $isPresented, selection: $selection)
.onChange(of: isPresented) { presented in
if !presented {
onComplete(selection)
}
}
}
}
@available(iOS 16.0, *)
final class FamilyActivityPickerPresenter: UIViewController {
private let initial: FamilyActivitySelection
var onComplete: ((FamilyActivitySelection) -> Void)?
init(selection: FamilyActivitySelection = FamilyActivitySelection()) {
self.initial = selection
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .overFullScreen
view.backgroundColor = .clear
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard children.isEmpty else { return }
let root = FamilyActivityPickerSheet(initial: initial) { [weak self] selection in
self?.onComplete?(selection)
self?.dismiss(animated: false)
}
let host = UIHostingController(rootView: root)
host.view.backgroundColor = .clear
addChild(host)
view.addSubview(host.view)
host.view.frame = .zero
host.didMove(toParent: self)
}
}
@available(iOS 16.0, *)
struct ApplicationTokenLabelView: View {
let token: ApplicationToken
var body: some View {
Label(token)
.labelStyle(.titleAndIcon)
.lineLimit(1)
}
}
@available(iOS 16.0, *)
struct ApplicationTokenIconView: View {
let token: ApplicationToken
var body: some View {
Label(token)
.labelStyle(.iconOnly)
.scaleEffect(2.4)
.frame(width: 40, height: 40)
.clipped()
}
}
@available(iOS 16.0, *)
final class ApplicationTokenIconHostingView: UIView {
private let hosting: UIHostingController<ApplicationTokenIconView>
init(token: ApplicationToken) {
hosting = UIHostingController(rootView: ApplicationTokenIconView(token: token))
super.init(frame: .zero)
hosting.view.backgroundColor = .clear
addSubview(hosting.view)
hosting.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hosting.view.topAnchor.constraint(equalTo: topAnchor),
hosting.view.leadingAnchor.constraint(equalTo: leadingAnchor),
hosting.view.trailingAnchor.constraint(equalTo: trailingAnchor),
hosting.view.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
func update(token: ApplicationToken) {
hosting.rootView = ApplicationTokenIconView(token: token)
}
}
@available(iOS 16.0, *)
final class ApplicationTokenLabelHostingView: UIView {
private let hosting: UIHostingController<ApplicationTokenLabelView>
init(token: ApplicationToken) {
hosting = UIHostingController(rootView: ApplicationTokenLabelView(token: token))
super.init(frame: .zero)
hosting.view.backgroundColor = .clear
addSubview(hosting.view)
hosting.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hosting.view.topAnchor.constraint(equalTo: topAnchor),
hosting.view.leadingAnchor.constraint(equalTo: leadingAnchor),
hosting.view.trailingAnchor.constraint(equalTo: trailingAnchor),
hosting.view.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
func update(token: ApplicationToken) {
hosting.rootView = ApplicationTokenLabelView(token: token)
}
}

View File

@ -0,0 +1,66 @@
//
// ITunesSearchService.swift
// QuickLocation
//
import Foundation
enum ITunesSearchError: Error {
case invalidURL
case invalidResponse
}
enum ITunesSearchService {
private struct SearchResponse: Decodable {
let results: [ITunesAppResult]
}
private struct ITunesAppResult: Decodable {
let trackId: Int
let trackName: String
let artworkUrl100: String?
let artworkUrl512: String?
}
static func search(term: String, limit: Int = 25) async throws -> [AppCatalogItem] {
let trimmed = term.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return [] }
var components = URLComponents(string: "https://itunes.apple.com/search")
components?.queryItems = [
URLQueryItem(name: "term", value: trimmed),
URLQueryItem(name: "entity", value: "software"),
URLQueryItem(name: "country", value: "cn"),
URLQueryItem(name: "limit", value: "\(limit)")
]
guard let url = components?.url else { throw ITunesSearchError.invalidURL }
let (data, response) = try await URLSession.shared.data(from: url)
guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode) else {
throw ITunesSearchError.invalidResponse
}
let decoded = try JSONDecoder().decode(SearchResponse.self, from: data)
return decoded.results.map { result in
let iconURL = result.artworkUrl512 ?? result.artworkUrl100
return AppCatalogItem(
id: "itunes:\(result.trackId)",
name: result.trackName,
icon: "",
keywords: [],
iconURL: iconURL
)
}
}
static func mergedResults(local: [AppCatalogItem], remote: [AppCatalogItem]) -> [AppCatalogItem] {
var seen = Set<String>()
var merged: [AppCatalogItem] = []
for item in local + remote {
guard !seen.contains(item.id) else { continue }
seen.insert(item.id)
merged.append(item)
}
return merged
}
}

View File

@ -0,0 +1,86 @@
//
// PairGuideStepsView.swift
// QuickLocation
//
import UIKit
final class PairGuideStepsView: UIView {
private struct Step {
let number: String
let title: String
let color: UIColor
}
private let steps: [Step] = [
Step(number: "1", title: "新增配对应用", color: UIColor(hexStr: "#8B5CF6")),
Step(number: "2", title: "选择相同应用", color: UIColor(hexStr: "#16B3FF")),
Step(number: "3", title: "完成配对", color: UIColor(hexStr: "#22C55E"))
]
private let circleSize: CGFloat = 28
private let dashLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
dashLayer.strokeColor = UIColor(hexStr: "#D1D5DB").cgColor
dashLayer.lineWidth = 1
dashLayer.lineDashPattern = [3, 3]
dashLayer.fillColor = UIColor.clear.cgColor
layer.addSublayer(dashLayer)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override var intrinsicContentSize: CGSize {
CGSize(width: UIView.noIntrinsicMetric, height: 72)
}
override func layoutSubviews() {
super.layoutSubviews()
subviews.forEach { $0.removeFromSuperview() }
let columnWidth = bounds.width / 3
let circleY = circleSize / 2 + 2
let centers = (0..<steps.count).map { columnWidth * (CGFloat($0) + 0.5) }
let path = UIBezierPath()
path.move(to: CGPoint(x: centers.first ?? 0, y: circleY))
path.addLine(to: CGPoint(x: centers.last ?? bounds.width, y: circleY))
dashLayer.path = path.cgPath
dashLayer.frame = bounds
for (index, step) in steps.enumerated() {
let centerX = centers[index]
let circle = UILabel(frame: CGRect(
x: centerX - circleSize / 2,
y: 0,
width: circleSize,
height: circleSize
))
circle.text = step.number
circle.font = FontManager.boboBold(14)
circle.textColor = .white
circle.textAlignment = .center
circle.backgroundColor = step.color
circle.layer.cornerRadius = circleSize / 2
circle.clipsToBounds = true
addSubview(circle)
let label = UILabel()
label.text = step.title
label.font = .systemFont(ofSize: 12, weight: .medium)
label.textColor = UIColor(hexStr: "#293445")
label.textAlignment = .center
label.frame = CGRect(
x: columnWidth * CGFloat(index) + 4,
y: circleSize + 10,
width: columnWidth - 8,
height: 34
)
addSubview(label)
}
}
}

View File

@ -0,0 +1,141 @@
//
// SelectActivityVC.swift
// QuickLocation
//
import UIKit
import RxSwift
import RxCocoa
import Kingfisher
final class SelectActivityCell: UITableViewCell {
static let reuseId = "SelectActivityCell"
private let iconView = UIImageView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
iconView.contentMode = .scaleAspectFill
iconView.layer.cornerRadius = 10
iconView.clipsToBounds = true
contentView.addSubview(iconView)
iconView.layoutChain.left(16).centerY().width(40).height(40)
textLabel?.layoutChain.leftToRightOfView(iconView, offset: 12).centerY().right(16)
selectionStyle = .default
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
func configure(item: AppCatalogItem) {
textLabel?.text = item.name
textLabel?.font = .systemFont(ofSize: 16, weight: .medium)
textLabel?.textColor = UIColor(hexStr: "#293445")
AppCatalogStore.setIcon(for: iconView, item: item)
}
override func prepareForReuse() {
super.prepareForReuse()
iconView.kf.cancelDownloadTask()
iconView.image = nil
}
}
final class SelectActivityVC: BaseViewController {
var onSelect: ((AppCatalogItem) -> Void)?
private let searchField = UITextField()
private let tableView = UITableView(frame: .zero, style: .plain)
private let activityIndicator = UIActivityIndicatorView(style: .medium)
private var items: [AppCatalogItem] = AppCatalogStore.sharedItems
private var searchTask: Task<Void, Never>?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
title = "选取应用"
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "取消", style: .plain, target: self, action: #selector(cancel))
searchField.placeholder = "搜索应用"
searchField.borderStyle = .roundedRect
searchField.font = .systemFont(ofSize: 15)
searchField.clearButtonMode = .whileEditing
searchField.returnKeyType = .search
searchField.addTarget(self, action: #selector(searchChanged), for: .editingChanged)
view.addSubview(searchField)
searchField.layoutChain.top(12).left(16).right(16).height(40)
activityIndicator.hidesWhenStopped = true
view.addSubview(activityIndicator)
activityIndicator.layoutChain.centerX().topToBottomOfView(searchField, offset: 8)
tableView.register(SelectActivityCell.self, forCellReuseIdentifier: SelectActivityCell.reuseId)
tableView.dataSource = self
tableView.delegate = self
tableView.rowHeight = 56
view.addSubview(tableView)
tableView.layoutChain.topToBottomOfView(searchField, offset: 12).edgesHorzontal().bottom()
}
@objc private func cancel() {
dismiss(animated: true)
}
@objc private func searchChanged() {
let query = searchField.text ?? ""
searchTask?.cancel()
let trimmed = query.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty {
activityIndicator.stopAnimating()
items = AppCatalogStore.sharedItems
tableView.reloadData()
return
}
let localHits = AppCatalogStore.search(query)
items = localHits
tableView.reloadData()
activityIndicator.startAnimating()
searchTask = Task { [weak self] in
try? await Task.sleep(nanoseconds: 300_000_000)
guard let self, !Task.isCancelled else { return }
do {
let remote = try await ITunesSearchService.search(term: trimmed)
guard !Task.isCancelled else { return }
await MainActor.run {
self.activityIndicator.stopAnimating()
self.items = ITunesSearchService.mergedResults(local: localHits, remote: remote)
self.tableView.reloadData()
}
} catch {
await MainActor.run {
self.activityIndicator.stopAnimating()
if self.items.isEmpty {
self.items = localHits
self.tableView.reloadData()
}
DLToast.show(text: "搜索失败,请检查网络")
}
}
}
}
}
extension SelectActivityVC: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: SelectActivityCell.reuseId, for: indexPath) as! SelectActivityCell
cell.configure(item: items[indexPath.row])
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let item = items[indexPath.row]
onSelect?(item)
dismiss(animated: true)
}
}

View File

@ -0,0 +1,50 @@
[
{
"id": "wechat",
"name": "微信",
"icon": "Login/wechat",
"keywords": ["微信", "wechat", "weixin"]
},
{
"id": "douyin",
"name": "抖音",
"icon": "AppRestrict/catalog_douyin",
"keywords": ["抖音", "douyin", "tiktok"]
},
{
"id": "qq",
"name": "QQ",
"icon": "AppRestrict/catalog_qq",
"keywords": ["qq"]
},
{
"id": "xiaohongshu",
"name": "小红书",
"icon": "AppRestrict/catalog_xiaohongshu",
"keywords": ["小红书", "red", "xhs"]
},
{
"id": "taobao",
"name": "淘宝",
"icon": "AppRestrict/catalog_taobao",
"keywords": ["淘宝", "taobao"]
},
{
"id": "netease_music",
"name": "网易云音乐",
"icon": "AppRestrict/catalog_netease",
"keywords": ["网易云", "music"]
},
{
"id": "kuaishou",
"name": "快手",
"icon": "AppRestrict/catalog_kuaishou",
"keywords": ["快手", "kuaishou"]
},
{
"id": "wangzhe",
"name": "王者荣耀",
"icon": "AppRestrict/catalog_wangzhe",
"keywords": ["王者", "荣耀"]
}
]

View File

@ -33,6 +33,8 @@ final class FeatureIntroVC: BaseViewController {
AppRouter.push(vc) AppRouter.push(vc)
case .createBubble: case .createBubble:
AppRouter.push(Route.createBubble) AppRouter.push(Route.createBubble)
case .lockDistract:
AppRouter.push(Route.lockDistract)
case .searchLocation: case .searchLocation:
AppRouter.push(Route.searchLocation) AppRouter.push(Route.searchLocation)
case .sos: case .sos:

View File

@ -18,6 +18,7 @@ struct FeatureIntroItem {
enum FeatureIntroAction { enum FeatureIntroAction {
case signIn case signIn
case createBubble case createBubble
case lockDistract
case searchLocation case searchLocation
case sos case sos
case placeholder case placeholder
@ -58,7 +59,7 @@ final class FeatureIntroView: UIView {
titleColor: UIColor(hexStr: "#2F8A5B"), titleColor: UIColor(hexStr: "#2F8A5B"),
subtitleColor: UIColor(hexStr: "#6AAD88"), subtitleColor: UIColor(hexStr: "#6AAD88"),
arrowTint: UIColor(hexStr: "#3EAE72"), arrowTint: UIColor(hexStr: "#3EAE72"),
action: .placeholder action: .lockDistract
), ),
FeatureIntroItem( FeatureIntroItem(
title: "飞鸽传书", title: "飞鸽传书",

View File

@ -23,6 +23,7 @@ class CreateGroupVC: BaseViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
view.backgroundColor = UIColor(hexStr: "#FAFAFA")
bindViewModel() bindViewModel()
reactiveAction() reactiveAction()
@ -31,18 +32,25 @@ class CreateGroupVC: BaseViewController {
} }
private func reactiveAction() { private func reactiveAction() {
rootView.groupIconInputView.rx.tapGesture.subscribe { _ in rootView.iconCarousel.onSelectIndex = { [weak self] index in
let vc = GroupIconListVC(iconIndex: "1") self?.viewModel.iconIndex = index
vc.onSelectIcon = { index in
self.viewModel.iconIndex = index
self.rootView.groupIconImgView.image = UIImage(named: "GroupIcon/\(index)")
} }
self.navigationController?.pushViewController(vc, animated: true)
}.disposed(by: disposeBag)
rootView.submitBtn.rx.tap.subscribe(onNext: { _ in rootView.submitBtn.rx.tap.subscribe(onNext: { [weak self] _ in
self.viewModel.requestCreateGroup() self?.viewModel.requestCreateGroup()
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
rootView.onConfirmAddTag = { [weak self] text in
guard let self = self else { return }
switch self.viewModel.addCustomTag(text) {
case .success:
self.rootView.dismissAddTagSheet()
case .empty:
DLToast.show(text: "请输入标签")
case .duplicate:
DLToast.show(text: "标签已存在")
}
}
} }
private func bindViewModel() { private func bindViewModel() {
@ -58,8 +66,29 @@ class CreateGroupVC: BaseViewController {
.bind(to: rootView.tagView.rx.items(dataSource: dataSource)) .bind(to: rootView.tagView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag) .disposed(by: disposeBag)
rootView.tagView.rx.modelSelected(String.self) viewModel.output.sectionedItems
.subscribe(viewModel.cellAction.inputs) .observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] _ in
DispatchQueue.main.async {
self?.rootView.updateTagViewHeight()
}
})
.disposed(by: disposeBag)
rootView.tagView.rx.itemSelected
.subscribe(onNext: { [weak self] indexPath in
guard let self = self else { return }
self.rootView.tagView.deselectItem(at: indexPath, animated: false)
let tag = self.dataSource[indexPath]
if tag == CreateGroupViewModel.addTagToken {
self.rootView.showAddTagSheet()
} else {
self.viewModel.cellAction.execute(tag)
}
})
.disposed(by: disposeBag)
rootView.tagView.rx.setDelegate(self)
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }
@ -67,8 +96,22 @@ class CreateGroupVC: BaseViewController {
private lazy var dataSource: RxCollectionViewSectionedReloadDataSource<GroupTagListSectionModel> = { private lazy var dataSource: RxCollectionViewSectionedReloadDataSource<GroupTagListSectionModel> = {
RxCollectionViewSectionedReloadDataSource<GroupTagListSectionModel> { datasource, collectionView, indexPath, item in RxCollectionViewSectionedReloadDataSource<GroupTagListSectionModel> { datasource, collectionView, indexPath, item in
let cell: TagCell = collectionView.dequeueReusableCell(for: indexPath) let cell: TagCell = collectionView.dequeueReusableCell(for: indexPath)
if item == CreateGroupViewModel.addTagToken {
cell.configureAsAdd()
} else {
cell.configure(item, isSelected: self.viewModel.isSelected(tag: item)) cell.configure(item, isSelected: self.viewModel.isSelected(tag: item))
}
return cell return cell
} }
}() }()
} }
extension CreateGroupVC: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
guard dataSource.sectionModels.indices.contains(indexPath.section),
dataSource.sectionModels[indexPath.section].items.indices.contains(indexPath.item) else {
return CGSize(width: 52, height: 28)
}
return TagCell.preferredSize(for: dataSource[indexPath])
}
}

View File

@ -14,6 +14,9 @@ class CreateGroupView: UIView {
var disposeBag = DisposeBag() var disposeBag = DisposeBag()
private let limitCount = 50 private let limitCount = 50
private let tagMaxLength = 10
var onConfirmAddTag: ((String) -> Void)?
private func setupRx() { private func setupRx() {
groupNameTF.rx.text.orEmpty groupNameTF.rx.text.orEmpty
@ -43,32 +46,57 @@ class CreateGroupView: UIView {
} }
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
addTagTF.rx.text.orEmpty
.subscribe(onNext: { [weak self] text in
guard let self = self else { return }
if text.count > self.tagMaxLength {
self.addTagTF.text = String(text.prefix(self.tagMaxLength))
}
let count = self.addTagTF.text?.count ?? 0
self.addTagCountLab.text = "\(count)/\(self.tagMaxLength)"
})
.disposed(by: disposeBag)
sheetCancelBtn.rx.tap
.subscribe(onNext: { [weak self] in
self?.dismissAddTagSheet()
})
.disposed(by: disposeBag)
sheetConfirmBtn.rx.tap
.subscribe(onNext: { [weak self] in
guard let self = self else { return }
self.onConfirmAddTag?(self.addTagTF.text ?? "")
})
.disposed(by: disposeBag)
addTagClearBtn.rx.tap
.subscribe(onNext: { [weak self] in
self?.addTagTF.text = ""
self?.addTagCountLab.text = "0/\(self?.tagMaxLength ?? 10)"
})
.disposed(by: disposeBag)
} }
private func setupUI() { private func setupUI() {
addSubview(navBgView) addSubview(navBgView)
addSubview(navView) addSubview(navView)
addSubview(scrollView)
addSubview(submitBtn) addSubview(submitBtn)
addSubview(titleLab)
addSubview(infoView) scrollView.addSubview(contentView)
infoView.addSubview(groupNameInputView) contentView.addSubview(iconCarousel)
groupNameInputView.addSubview(groupNameTitleLab) contentView.addSubview(typeNamePlaceholder)
contentView.addSubview(groupNameTitleLab)
contentView.addSubview(groupNameInputView)
groupNameInputView.addSubview(groupNameTF) groupNameInputView.addSubview(groupNameTF)
contentView.addSubview(groupContentTitleLab)
infoView.addSubview(groupIconInputView) contentView.addSubview(groupContentInputView)
groupIconInputView.addSubview(groupIconTitleLab)
groupIconInputView.addSubview(groupIconImgView)
infoView.addSubview(groupContentInputView)
groupContentInputView.addSubview(groupContentTitleLab)
groupContentInputView.addSubview(groupContentTV) groupContentInputView.addSubview(groupContentTV)
groupContentInputView.addSubview(placeholderLab) groupContentInputView.addSubview(placeholderLab)
contentView.addSubview(tagTitleLab)
infoView.addSubview(tagInfoView) contentView.addSubview(tagView)
tagInfoView.addSubview(tagTitleLab)
tagInfoView.addSubview(tagView)
tagInfoView.addSubview(tipsLab)
navBgView.layoutChain navBgView.layoutChain
.edges(excludingEdge: .bottom) .edges(excludingEdge: .bottom)
@ -78,91 +106,205 @@ class CreateGroupView: UIView {
.edges(excludingEdge: .bottom) .edges(excludingEdge: .bottom)
.height(kNaviHeight) .height(kNaviHeight)
titleLab.layoutChain
.topToBottomOfView(navView, offset: 20)
.left(15)
submitBtn.layoutChain submitBtn.layoutChain
.bottom(kSafeBottomMargin + 36) .bottom(kSafeBottomMargin + 24)
.centerX() .centerX()
.edgesHorzontal(30) .edgesHorzontal(30)
.height(50) .height(56)
infoView.layoutChain scrollView.layoutChain
.topToBottomOfView(titleLab, offset: 20) .topToBottomOfView(navView)
.edgesHorzontal(15) .edgesHorzontal()
.bottomToTopOfView(submitBtn, offset: -12)
groupNameInputView.layoutChain contentView.layoutChain
.edges(excludingEdge: .bottom) .edges()
.widthToView(scrollView)
iconCarousel.layoutChain
.top(8)
.edgesHorzontal()
.height(130)
typeNamePlaceholder.layoutChain
.topToBottomOfView(iconCarousel, offset: 4)
.centerX()
groupNameTitleLab.layoutChain groupNameTitleLab.layoutChain
.left(15) .topToBottomOfView(typeNamePlaceholder, offset: 24)
.centerY() .left(24)
.width(50)
groupNameInputView.layoutChain
.topToBottomOfView(groupNameTitleLab, offset: 10)
.edgesHorzontal(20)
.height(50)
groupNameTF.layoutChain groupNameTF.layoutChain
.edgesVertical() .edgesVertical()
.leftToRightOfView(groupNameTitleLab, offset: 20) .left(16)
.right(43) .right(48)
groupIconInputView.layoutChain
.topToBottomOfView(groupNameInputView)
.leftToView(groupNameInputView)
.rightToView(groupNameInputView)
groupIconTitleLab.layoutChain
.left(15)
.centerY()
.width(50)
groupIconImgView.layoutChain
.leftToRightOfView(groupIconTitleLab, offset: 20)
.edgesVertical(10)
.width(40)
.height(40)
groupContentInputView.layoutChain
.topToBottomOfView(groupIconInputView)
.leftToView(groupNameInputView)
.rightToView(groupNameInputView)
groupContentTitleLab.layoutChain groupContentTitleLab.layoutChain
.top(20) .topToBottomOfView(groupNameInputView, offset: 18)
.left(15) .left(24)
.width(50)
groupContentInputView.layoutChain
.topToBottomOfView(groupContentTitleLab, offset: 10)
.edgesHorzontal(20)
.height(96)
groupContentTV.layoutChain groupContentTV.layoutChain
.topToView(groupContentTitleLab, offset: -9) .top(8)
.leftToRightOfView(groupContentTitleLab, offset: 18) .left(12)
.right(52) .right(48)
.bottom(10) .bottom(8)
.height(20, relation: .greaterThanOrEqual)
placeholderLab.layoutChain placeholderLab.layoutChain
.topToView(groupContentTV, offset: 8) .topToView(groupContentTV, offset: 8)
.leftToView(groupContentTV, offset: 5) .leftToView(groupContentTV, offset: 5)
tagInfoView.layoutChain
.topToBottomOfView(groupContentInputView)
.leftToView(groupNameInputView)
.rightToView(groupNameInputView)
.bottom(20)
tagTitleLab.layoutChain tagTitleLab.layoutChain
.top(20) .topToBottomOfView(groupContentInputView, offset: 18)
.left(15) .left(24)
tagView.layoutChain tagView.layoutChain
.topToBottomOfView(tagTitleLab, offset: 10) .topToBottomOfView(tagTitleLab, offset: 12)
.edgesHorzontal(15) .edgesHorzontal(20)
.height(64) .height(28)
.bottom(24)
tipsLab.layoutChain setupSheet()
.topToBottomOfView(tagView, offset: 10)
.left(15)
.bottom()
} }
func updateTagViewHeight() {
tagView.collectionViewLayout.invalidateLayout()
tagView.layoutIfNeeded()
let height = max(28, tagView.collectionViewLayout.collectionViewContentSize.height)
tagView.layoutChain.height(height)
}
func showAddTagSheet() {
addTagTF.text = ""
addTagCountLab.text = "0/\(tagMaxLength)"
sheetOverlay.isHidden = false
sheetOverlay.alpha = 0
sheetPanel.transform = CGAffineTransform(translationX: 0, y: 40)
UIView.animate(withDuration: 0.28, delay: 0, options: .curveEaseOut) {
self.sheetOverlay.alpha = 1
self.sheetPanel.transform = .identity
} completion: { _ in
self.addTagTF.becomeFirstResponder()
}
}
func dismissAddTagSheet() {
addTagTF.resignFirstResponder()
UIView.animate(withDuration: 0.22, delay: 0, options: .curveEaseIn) {
self.sheetOverlay.alpha = 0
self.sheetPanel.transform = CGAffineTransform(translationX: 0, y: 40)
} completion: { _ in
self.sheetOverlay.isHidden = true
self.addTagTF.text = ""
self.addTagCountLab.text = "0/\(self.tagMaxLength)"
}
}
// MARK: - Sheet
private func setupSheet() {
sheetOverlay.backgroundColor = UIColor.black.withAlphaComponent(0.45)
sheetOverlay.isHidden = true
addSubview(sheetOverlay)
sheetOverlay.layoutChain.edges()
sheetPanel.backgroundColor = .white
sheetPanel.layer.cornerRadius = 24
sheetPanel.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
sheetPanel.clipsToBounds = true
sheetOverlay.addSubview(sheetPanel)
sheetPanel.layoutChain.edgesHorzontal().bottom()
sheetHeaderView.backgroundColor = UIColor(hexStr: "#E8F7FF")
sheetPanel.addSubview(sheetHeaderView)
sheetHeaderView.layoutChain.edgesHorzontal().top().height(56)
sheetTitleLab.text = "添加标签"
sheetTitleLab.font = .systemFont(ofSize: 16, weight: .medium)
sheetTitleLab.textColor = ThemeManager.shared.color.titleAuxColor
sheetTitleLab.textAlignment = .center
sheetHeaderView.addSubview(sheetTitleLab)
sheetTitleLab.layoutChain.center()
sheetAddIcon.backgroundColor = UIColor(hexStr: "#D6F0FF")
sheetAddIcon.cornerRadius = 8
sheetAddIcon.contentMode = .center
let plusConfig = UIImage.SymbolConfiguration(pointSize: 13, weight: .medium)
sheetAddIcon.image = UIImage(systemName: "plus", withConfiguration: plusConfig)
sheetAddIcon.tintColor = UIColor(hexStr: "#16B3FF")
sheetPanel.addSubview(sheetAddIcon)
sheetAddIcon.layoutChain
.topToBottomOfView(sheetHeaderView, offset: 24)
.left(20)
.width(32)
.height(32)
addTagInputView.backgroundColor = UIColor(hexStr: "#F7F8FA")
addTagInputView.cornerRadius = 10
sheetPanel.addSubview(addTagInputView)
addTagClearBtn.setImage(UIImage(systemName: "xmark.circle.fill"), for: .normal)
addTagClearBtn.tintColor = UIColor(hexStr: "#FF5B5B")
sheetPanel.addSubview(addTagClearBtn)
addTagClearBtn.layoutChain
.centerY(sheetAddIcon)
.right(20)
.width(22)
.height(22)
addTagInputView.layoutChain
.centerY(sheetAddIcon)
.leftToRightOfView(sheetAddIcon, offset: 10)
.rightToLeftOfView(addTagClearBtn, offset: -10)
.height(40)
addTagInputView.addSubview(addTagTF)
addTagInputView.addSubview(addTagCountLab)
addTagCountLab.layoutChain
.centerY()
.right(12)
addTagTF.layoutChain
.edgesVertical()
.left(12)
.rightToLeftOfView(addTagCountLab, offset: -8)
sheetPanel.addSubview(sheetCancelBtn)
sheetPanel.addSubview(sheetConfirmBtn)
sheetCancelBtn.layoutChain
.topToBottomOfView(sheetAddIcon, offset: 28)
.left(24)
.height(44)
.bottom(20 + kSafeBottomMargin)
sheetConfirmBtn.layoutChain
.leftToRightOfView(sheetCancelBtn, offset: 12)
.right(24)
.bottomToView(sheetCancelBtn)
.heightToView(sheetCancelBtn)
.widthToView(sheetCancelBtn)
let tap = UITapGestureRecognizer(target: self, action: #selector(tapSheetMask))
sheetOverlay.addGestureRecognizer(tap)
}
@objc private func tapSheetMask(_ gesture: UITapGestureRecognizer) {
let point = gesture.location(in: sheetPanel)
if sheetPanel.bounds.contains(point) { return }
dismissAddTagSheet()
}
// MARK: - Views
lazy var navBgView: UIImageView = { lazy var navBgView: UIImageView = {
let iv = UIImageView() let iv = UIImageView()
iv.image = UIImage(named: "Common/navBar_bg_2") iv.image = UIImage(named: "Common/navBar_bg_2")
@ -175,54 +317,73 @@ class CreateGroupView: UIView {
return nav return nav
}() }()
lazy var titleLab: UILabel = { lazy var scrollView: UIScrollView = {
let label = UILabel() let view = UIScrollView()
label.text = "编辑信息" view.backgroundColor = .clear
label.font = .systemFont(ofSize: 14, weight: .medium) view.keyboardDismissMode = .onDrag
label.textColor = ThemeManager.shared.color.titleAuxColor view.showsVerticalScrollIndicator = false
return label
}()
lazy var infoView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#F5FBFF")
view.cornerRadius = 10
return view return view
}() }()
// lazy var contentView: UIView = {
lazy var groupNameInputView: UIView = {
let view = UIView() let view = UIView()
view.backgroundColor = .clear view.backgroundColor = .clear
return view
}()
let icon = UIImageView() lazy var iconCarousel: CreateGroupIconCarouselView = {
icon.image = UIImage(named: "Group/edit") let view = CreateGroupIconCarouselView()
view.addSubview(icon) return view
icon.layoutChain }()
.right(15)
.edgesVertical(20)
.width(20)
.height(20)
let line = UIView() lazy var typeNamePlaceholder: UIView = {
line.backgroundColor = UIColor(hexStr: "#EEEEEE") let view = UIView()
view.addSubview(line) view.backgroundColor = .white
line.layoutChain view.cornerRadius = 10
.edgesHorzontal(15) view.isUserInteractionEnabled = false
.height(0.5)
.bottom() view.addSubview(typeNameLab)
typeNameLab.layoutChain
.edgesVertical(10)
.edgesHorzontal(31)
return view return view
}() }()
lazy var typeNameLab: UILabel = {
let label = UILabel()
label.text = "未知"
label.font = .systemFont(ofSize: 16, weight: .bold)
label.textColor = UIColor(hexStr: "#3D3D3D")
return label
}()
lazy var groupNameTitleLab: UILabel = { lazy var groupNameTitleLab: UILabel = {
let label = UILabel() let label = UILabel()
label.text = "圈子名称" label.text = "圈子名称"
label.font = .systemFont(ofSize: 12, weight: .medium) label.font = .systemFont(ofSize: 14, weight: .medium)
label.textColor = ThemeManager.shared.color.titleAuxColor label.textColor = ThemeManager.shared.color.titleAuxColor
return label return label
}() }()
lazy var groupNameInputView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.cornerRadius = 10
let icon = UIImageView()
icon.image = UIImage(named: "Group/edit")
icon.contentMode = .scaleAspectFit
view.addSubview(icon)
icon.layoutChain
.right(14)
.centerY()
.width(22)
.height(22)
return view
}()
lazy var groupNameTF: UITextField = { lazy var groupNameTF: UITextField = {
let textField = UITextField(frame: .zero) let textField = UITextField(frame: .zero)
textField.font = UIFont.systemFont(ofSize: 14, weight: .medium) textField.font = UIFont.systemFont(ofSize: 14, weight: .medium)
@ -232,119 +393,63 @@ class CreateGroupView: UIView {
return textField return textField
}() }()
//
lazy var groupIconInputView: UIView = {
let view = UIView()
view.backgroundColor = .clear
let icon = UIImageView()
icon.image = UIImage(named: "Group/arrow")
view.addSubview(icon)
icon.layoutChain
.right(15)
.centerY()
.width(14)
.height(14)
let line = UIView()
line.backgroundColor = UIColor(hexStr: "#EEEEEE")
view.addSubview(line)
line.layoutChain
.edgesHorzontal(15)
.height(0.5)
.bottom()
return view
}()
lazy var groupIconTitleLab: UILabel = {
let label = UILabel()
label.text = "圈子图标"
label.font = .systemFont(ofSize: 12, weight: .medium)
label.textColor = ThemeManager.shared.color.titleAuxColor
return label
}()
lazy var groupIconImgView: UIImageView = {
let view = UIImageView()
view.image = UIImage(named: "GroupIcon/1")
view.contentMode = .scaleAspectFill
return view
}()
//
lazy var groupContentInputView: UIView = {
let view = UIView()
view.backgroundColor = .clear
let icon = UIImageView()
icon.image = UIImage(named: "Group/edit")
view.addSubview(icon)
icon.layoutChain
.top(20)
.right(15)
.width(20)
.height(20)
let line = UIView()
line.backgroundColor = UIColor(hexStr: "#EEEEEE")
view.addSubview(line)
line.layoutChain
.edgesHorzontal(15)
.height(0.5)
.bottom()
return view
}()
lazy var groupContentTitleLab: UILabel = { lazy var groupContentTitleLab: UILabel = {
let label = UILabel() let label = UILabel()
label.text = "圈子描述" label.text = "圈子描述"
label.font = .systemFont(ofSize: 12, weight: .medium) label.font = .systemFont(ofSize: 14, weight: .medium)
label.textColor = ThemeManager.shared.color.titleAuxColor label.textColor = ThemeManager.shared.color.titleAuxColor
return label return label
}() }()
lazy var groupContentInputView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.cornerRadius = 10
let icon = UIImageView()
icon.image = UIImage(named: "Group/edit")
icon.contentMode = .scaleAspectFit
view.addSubview(icon)
icon.layoutChain
.right(14)
.bottom(14)
.width(22)
.height(22)
return view
}()
lazy var groupContentTV: UITextView = { lazy var groupContentTV: UITextView = {
let textView = UITextView() let textView = UITextView()
textView.backgroundColor = .clear textView.backgroundColor = .clear
textView.font = .systemFont(ofSize: 14, weight: .medium) textView.font = .systemFont(ofSize: 14, weight: .medium)
textView.textColor = ThemeManager.shared.color.titleAuxColor textView.textColor = ThemeManager.shared.color.titleAuxColor
textView.isScrollEnabled = false textView.isScrollEnabled = true
textView.textContainerInset = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0)
return textView return textView
}() }()
lazy var placeholderLab: UILabel = { lazy var placeholderLab: UILabel = {
let label = UILabel() let label = UILabel()
label.text = "请输入圈子描述" label.text = "请输入.."
label.textColor = ThemeManager.shared.color.contentColor label.textColor = ThemeManager.shared.color.contentColor
label.font = .systemFont(ofSize: 14, weight: .medium) label.font = .systemFont(ofSize: 14, weight: .medium)
return label return label
}() }()
//
lazy var tagInfoView: UIView = {
let view = UIView()
view.backgroundColor = .clear
return view
}()
lazy var tagTitleLab: UILabel = { lazy var tagTitleLab: UILabel = {
let label = UILabel() let label = UILabel()
label.text = "选择标签" label.text = "选择标签"
label.font = .systemFont(ofSize: 12, weight: .medium) label.font = .systemFont(ofSize: 14, weight: .medium)
label.textColor = ThemeManager.shared.color.titleAuxColor label.textColor = ThemeManager.shared.color.titleAuxColor
return label return label
}() }()
lazy var tagView: UICollectionView = { lazy var tagView: UICollectionView = {
let layout = UICollectionViewFlowLayout() let layout = CreateGroupTagFlowLayout()
let cvWidth = kScreenWidth - 60 layout.minimumInteritemSpacing = 8
let spacing: CGFloat = 6 layout.minimumLineSpacing = 8
let itemW = (cvWidth - spacing * 3) / 4 layout.sectionInset = .zero
layout.itemSize = CGSize(width: itemW, height: 27)
layout.minimumInteritemSpacing = spacing
layout.minimumLineSpacing = 10
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .clear cv.backgroundColor = .clear
@ -353,30 +458,74 @@ class CreateGroupView: UIView {
return cv return cv
}() }()
lazy var tipsLab: UILabel = {
let label = UILabel()
label.text = "如选择为私密圈子,将不能被分享到探索和被搜索。"
label.textColor = ThemeManager.shared.color.contentColor
label.font = .systemFont(ofSize: 10, weight: .regular)
return label
}()
lazy var submitBtn: UIButton = { lazy var submitBtn: UIButton = {
let btn = UIButton(type: .custom) let btn = UIButton(type: .custom)
btn.setTitle("创建", for: .normal) btn.setTitle("创建", for: .normal)
btn.setTitleColor(.white, for: .normal) btn.setTitleColor(.white, for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) btn.titleLabel?.font = FontManager.boboBold(18)
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal) btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
btn.cornerRadius = 25 btn.cornerRadius = 20
return btn
}()
private let sheetOverlay = UIView()
private let sheetPanel = UIView()
private let sheetHeaderView = UIView()
private let sheetTitleLab = UILabel()
private let sheetAddIcon = UIImageView()
private let addTagInputView = UIView()
lazy var addTagTF: UITextField = {
let textField = UITextField(frame: .zero)
textField.font = .systemFont(ofSize: 14, weight: .medium)
textField.textColor = ThemeManager.shared.color.titleAuxColor
textField.placeholderColor(placeholder: "请输入标签",
color: UIColor(hexStr: "#999999", alpha: 1.0),
font: .systemFont(ofSize: 14, weight: .medium))
return textField
}()
lazy var addTagCountLab: UILabel = {
let label = UILabel()
label.text = "0/10"
label.font = .systemFont(ofSize: 12, weight: .regular)
label.textColor = ThemeManager.shared.color.contentColor
return label
}()
lazy var addTagClearBtn: UIButton = {
let btn = UIButton(type: .custom)
return btn
}()
lazy var sheetCancelBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("取消", for: .normal)
btn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 15, weight: .medium)
btn.backgroundColor = .white
btn.borderWidth = 1
btn.borderColor = UIColor(hexStr: "#16B3FF")
btn.cornerRadius = 22
return btn
}()
lazy var sheetConfirmBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("确定", for: .normal)
btn.setTitleColor(.white, for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 15, weight: .medium)
btn.setBackgroundImage(UIImage(named: "Common/gradient_bg"), for: .normal)
btn.cornerRadius = 22
btn.clipsToBounds = true
return btn return btn
}() }()
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: .zero) super.init(frame: .zero)
backgroundColor = .white
setupUI() setupUI()
setupRx() setupRx()
backgroundColor = UIColor(hexStr: "#FAFAFA")
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
@ -384,7 +533,234 @@ class CreateGroupView: UIView {
} }
} }
// MARK: - Icon Carousel
final class CreateGroupIconCarouselView: UIView {
var onSelectIndex: ((Int) -> Void)?
private let iconCount = 11
private let itemSize: CGFloat = 80
private var lastWidth: CGFloat = 0
var selectedIndex: Int = 1 {
didSet {
guard oldValue != selectedIndex else { return }
onSelectIndex?(selectedIndex)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
clipsToBounds = false
addSubview(collectionView)
collectionView.layoutChain.edges()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
guard bounds.width > 0 else { return }
if bounds.width != lastWidth {
lastWidth = bounds.width
let inset = max(0, (bounds.width - itemSize) / 2)
flowLayout.sectionInset = UIEdgeInsets(top: 25, left: inset, bottom: 25, right: inset)
collectionView.layoutIfNeeded()
scrollToIndex(selectedIndex, animated: false)
}
applyTransforms()
}
private func scrollToIndex(_ index: Int, animated: Bool) {
let item = max(0, min(iconCount - 1, index - 1))
collectionView.scrollToItem(at: IndexPath(item: item, section: 0),
at: .centeredHorizontally,
animated: animated)
}
private func nearestIndex() -> Int {
let centerX = collectionView.contentOffset.x + collectionView.bounds.width / 2
var nearest = 0
var minDist = CGFloat.greatestFiniteMagnitude
for i in 0..<iconCount {
guard let attr = collectionView.layoutAttributesForItem(at: IndexPath(item: i, section: 0)) else { continue }
let dist = abs(attr.center.x - centerX)
if dist < minDist {
minDist = dist
nearest = i
}
}
return nearest
}
private func applyTransforms() {
let midX = collectionView.contentOffset.x + collectionView.bounds.width / 2
for cell in collectionView.visibleCells {
guard let attr = collectionView.layoutAttributesForItem(at: collectionView.indexPath(for: cell) ?? IndexPath()) else { continue }
let distance = abs(attr.center.x - midX)
let scale = max(0.72, 1.25 - distance / 180)
let alpha = max(0.45, 1 - distance / 220)
cell.transform = CGAffineTransform(scaleX: scale, y: scale)
cell.alpha = alpha
}
}
private lazy var flowLayout: UICollectionViewFlowLayout = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.itemSize = CGSize(width: itemSize, height: itemSize)
layout.minimumLineSpacing = 18
layout.minimumInteritemSpacing = 0
return layout
}()
private lazy var collectionView: UICollectionView = {
let cv = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
cv.backgroundColor = .clear
cv.showsHorizontalScrollIndicator = false
cv.decelerationRate = .fast
cv.clipsToBounds = false
cv.register(CreateGroupIconCell.self)
cv.dataSource = self
cv.delegate = self
return cv
}()
}
extension CreateGroupIconCarouselView: UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
iconCount
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(for: indexPath) as CreateGroupIconCell
cell.configure(UIImage(named: "GroupIcon/\(indexPath.item + 1)"))
return cell
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
applyTransforms()
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedIndex = indexPath.item + 1
collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
applyTransforms()
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let proposedCenter = targetContentOffset.pointee.x + scrollView.bounds.width / 2
var nearest = 0
var minDist = CGFloat.greatestFiniteMagnitude
for i in 0..<iconCount {
guard let attr = collectionView.layoutAttributesForItem(at: IndexPath(item: i, section: 0)) else { continue }
let dist = abs(attr.center.x - proposedCenter)
if dist < minDist {
minDist = dist
nearest = i
}
}
if let attr = collectionView.layoutAttributesForItem(at: IndexPath(item: nearest, section: 0)) {
targetContentOffset.pointee.x = attr.center.x - scrollView.bounds.width / 2
}
selectedIndex = nearest + 1
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
selectedIndex = nearestIndex() + 1
applyTransforms()
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
applyTransforms()
}
}
final class CreateGroupIconCell: UICollectionViewCell {
private let imageView: UIImageView = {
let view = UIImageView()
view.contentMode = .scaleAspectFill
view.clipsToBounds = true
view.cornerRadius = 18
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(imageView)
imageView.layoutChain.edges()
contentView.clipsToBounds = false
clipsToBounds = false
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(_ image: UIImage?) {
imageView.image = image
}
override func prepareForReuse() {
super.prepareForReuse()
transform = .identity
alpha = 1
}
}
// MARK: - Tag Flow Layout
final class CreateGroupTagFlowLayout: UICollectionViewFlowLayout {
private var alignedAttrs: [IndexPath: UICollectionViewLayoutAttributes] = [:]
override func prepare() {
super.prepare()
alignedAttrs.removeAll()
let rect = CGRect(origin: .zero, size: collectionViewContentSize)
guard let original = super.layoutAttributesForElements(in: rect) else { return }
let attrs = original.map { $0.copy() as! UICollectionViewLayoutAttributes }
leftAlign(attrs)
attrs.forEach { alignedAttrs[$0.indexPath] = $0 }
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
alignedAttrs.values.filter { $0.frame.intersects(rect) }
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
alignedAttrs[indexPath] ?? super.layoutAttributesForItem(at: indexPath)
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
collectionView?.bounds.width != newBounds.width
}
private func leftAlign(_ attrs: [UICollectionViewLayoutAttributes]) {
var x = sectionInset.left
var y = sectionInset.top
var lineHeight: CGFloat = 0
let maxX = (collectionView?.bounds.width ?? 0) - sectionInset.right
for attr in attrs where attr.representedElementCategory == .cell {
if x > sectionInset.left && x + attr.frame.width > maxX {
x = sectionInset.left
y += lineHeight + minimumLineSpacing
lineHeight = 0
}
attr.frame.origin = CGPoint(x: x, y: y)
x += attr.frame.width + minimumInteritemSpacing
lineHeight = max(lineHeight, attr.frame.height)
}
}
}
// MARK: - TagCell // MARK: - TagCell
final class TagCell: UICollectionViewCell { final class TagCell: UICollectionViewCell {
static let reuseId = "TagCell" static let reuseId = "TagCell"
@ -395,14 +771,26 @@ final class TagCell: UICollectionViewCell {
return l return l
}() }()
private let plusView: UIImageView = {
let view = UIImageView()
let config = UIImage.SymbolConfiguration(pointSize: 13, weight: .medium)
view.image = UIImage(systemName: "plus", withConfiguration: config)
view.tintColor = .white
view.contentMode = .center
view.isHidden = true
return view
}()
private var isTagSelected = false private var isTagSelected = false
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
contentView.addSubview(label) contentView.addSubview(label)
contentView.addSubview(plusView)
label.layoutChain.edges() label.layoutChain.edges()
contentView.layer.cornerRadius = 4 plusView.layoutChain.edges()
contentView.backgroundColor = UIColor(hexStr: "#E3F6FF") contentView.layer.cornerRadius = 8
contentView.backgroundColor = UIColor(hexStr: "#F0F2F5")
updateStyle() updateStyle()
} }
@ -410,12 +798,41 @@ final class TagCell: UICollectionViewCell {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func prepareForReuse() {
super.prepareForReuse()
plusView.isHidden = true
label.isHidden = false
label.text = nil
}
static func preferredSize(for item: String) -> CGSize {
if item == CreateGroupViewModel.addTagToken {
return CGSize(width: 28, height: 28)
}
let font = UIFont.systemFont(ofSize: 12, weight: .medium)
let textW = ceil((item as NSString).size(withAttributes: [.font: font]).width)
return CGSize(width: max(52, textW + 20), height: 28)
}
func configure(_ text: String, isSelected: Bool) { func configure(_ text: String, isSelected: Bool) {
plusView.isHidden = true
label.isHidden = false
label.text = text label.text = text
self.isTagSelected = isSelected isTagSelected = isSelected
contentView.layer.cornerRadius = 8
updateStyle() updateStyle()
} }
func configureAsAdd() {
plusView.isHidden = false
label.isHidden = true
label.text = nil
isTagSelected = false
contentView.backgroundColor = UIColor(hexStr: "#16B3FF")
contentView.layer.borderWidth = 0
contentView.layer.cornerRadius = 8
}
func toggleSelection() { func toggleSelection() {
isTagSelected.toggle() isTagSelected.toggle()
updateStyle() updateStyle()
@ -424,10 +841,12 @@ final class TagCell: UICollectionViewCell {
private func updateStyle() { private func updateStyle() {
if isTagSelected { if isTagSelected {
label.textColor = UIColor(hexStr: "#16B3FF") label.textColor = UIColor(hexStr: "#16B3FF")
contentView.backgroundColor = UIColor(hexStr: "#E3F6FF")
contentView.layer.borderWidth = 1 contentView.layer.borderWidth = 1
contentView.layer.borderColor = UIColor(hexStr: "#16B3FF").cgColor contentView.layer.borderColor = UIColor(hexStr: "#16B3FF").cgColor
} else { } else {
label.textColor = ThemeManager.shared.color.titleAuxColor label.textColor = ThemeManager.shared.color.titleAuxColor
contentView.backgroundColor = UIColor(hexStr: "#F0F2F5")
contentView.layer.borderWidth = 0 contentView.layer.borderWidth = 0
} }
} }

View File

@ -13,6 +13,15 @@ import SwiftyUserDefaults
typealias GroupTagListSectionModel = SectionModel<String, String> typealias GroupTagListSectionModel = SectionModel<String, String>
class CreateGroupViewModel { class CreateGroupViewModel {
enum AddTagResult {
case success
case empty
case duplicate
}
static let addTagToken = "__add_tag__"
static let tagMaxLength = 10
struct Input { struct Input {
} }
@ -26,7 +35,7 @@ class CreateGroupViewModel {
private let sectionedItems = PublishSubject<[GroupTagListSectionModel]>() private let sectionedItems = PublishSubject<[GroupTagListSectionModel]>()
private let tagList = ["私密", "游戏", "运动", "美食", private var tagList = ["私密", "游戏", "运动", "美食",
"自驾", "聚会", "旅行", "学习"] "自驾", "聚会", "旅行", "学习"]
var selectedTagList: [String] = [] { var selectedTagList: [String] = [] {
@ -56,9 +65,21 @@ class CreateGroupViewModel {
return selectedTagList.first(where: { tag == $0 }) != nil return selectedTagList.first(where: { tag == $0 }) != nil
} }
func addCustomTag(_ raw: String) -> AddTagResult {
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return .empty }
let tag = String(trimmed.prefix(Self.tagMaxLength))
if tagList.contains(tag) {
return .duplicate
}
tagList.append(tag)
selectedTagList.append(tag)
return .success
}
// MARK: - // MARK: -
func loadData() { func loadData() {
sectionedItems.onNext(tagList.mapSection()) sectionedItems.onNext((tagList + [Self.addTagToken]).mapSection())
} }
// MARK: - Request // MARK: - Request

View File

@ -642,7 +642,7 @@ final class CircleGroupCell: UITableViewCell {
private let iconView: UIImageView = { private let iconView: UIImageView = {
let iv = UIImageView() let iv = UIImageView()
iv.contentMode = .scaleAspectFill iv.contentMode = .scaleAspectFill
iv.cornerRadius = 20 iv.cornerRadius = 10
iv.clipsToBounds = true iv.clipsToBounds = true
iv.backgroundColor = UIColor(hexStr: "#F0F0F0") iv.backgroundColor = UIColor(hexStr: "#F0F0F0")
return iv return iv

View File

@ -20,8 +20,8 @@ class JoinGroupVC: BaseViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
view.backgroundColor = .white
// Do any additional setup after loading the view.
rootView.textField.rx.controlEvent(.editingDidEndOnExit).subscribe(onNext: { rootView.textField.rx.controlEvent(.editingDidEndOnExit).subscribe(onNext: {
self.rootView.textField.resignFirstResponder() self.rootView.textField.resignFirstResponder()
self.requestOperateGroup() self.requestOperateGroup()
@ -31,6 +31,10 @@ class JoinGroupVC: BaseViewController {
self.requestOperateGroup() self.requestOperateGroup()
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
rootView.keyboardConfirmBtn.rx.tap.subscribe(onNext: { _ in
self.rootView.textField.resignFirstResponder()
}).disposed(by: disposeBag)
rootView.scanBtn.rx.tap.subscribe(onNext: { _ in rootView.scanBtn.rx.tap.subscribe(onNext: { _ in
let vc = ScanVC { code in let vc = ScanVC { code in
self.rootView.textField.text = code self.rootView.textField.text = code

View File

@ -16,7 +16,14 @@ class JoinGroupView: UIView {
var numberBtns: [UIButton] = [] var numberBtns: [UIButton] = []
var inviteCode: String = "" { var inviteCode: String = "" {
didSet { didSet { updateCodeBoxes() }
}
@objc func numAction(button: UIButton) {
textField.becomeFirstResponder()
}
private func updateCodeBoxes() {
for btn in numberBtns { for btn in numberBtns {
btn.setTitle("", for: .normal) btn.setTitle("", for: .normal)
btn.isSelected = false btn.isSelected = false
@ -25,16 +32,13 @@ class JoinGroupView: UIView {
let characters = inviteCode.map { String($0) } let characters = inviteCode.map { String($0) }
for (index, character) in characters.enumerated() { for (index, character) in characters.enumerated() {
numberBtns[index].setTitle(character, for: .normal) numberBtns[index].setTitle(character, for: .normal)
if index < numberBtns.count-1 { numberBtns[index].isSelected = true
numberBtns[index].isSelected = false
numberBtns[index+1].isSelected = true
}
}
}
} }
@objc func numAction(button: UIButton) { let cursorIndex = characters.count
textField.becomeFirstResponder() if cursorIndex < numberBtns.count {
numberBtns[cursorIndex].isSelected = true
}
} }
private func setupRx() { private func setupRx() {
@ -43,223 +47,234 @@ class JoinGroupView: UIView {
.subscribe(onNext: { text in .subscribe(onNext: { text in
if text.count < 7 { if text.count < 7 {
self.inviteCode = text self.inviteCode = text
} } else {
else {
self.inviteCode = String(text.dropLast(2) + [text.last!]) self.inviteCode = String(text.dropLast(2) + [text.last!])
self.textField.text = self.inviteCode self.textField.text = self.inviteCode
} }
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }
private func setupUI() { private func setupUI() {
addSubview(navBgView) addSubview(navBgView)
addSubview(contentCard)
addSubview(navView) addSubview(navView)
addSubview(titleLab)
addSubview(number1)
addSubview(number2)
addSubview(number3)
addSubview(lineView)
addSubview(number4)
addSubview(number5)
addSubview(number6)
addSubview(tipsLab)
addSubview(submitBtn)
addSubview(textField) addSubview(textField)
addSubview(scanBtn)
contentCard.addSubview(cardTitleLab)
contentCard.addSubview(number1)
contentCard.addSubview(number2)
contentCard.addSubview(number3)
contentCard.addSubview(lineView)
contentCard.addSubview(number4)
contentCard.addSubview(number5)
contentCard.addSubview(number6)
contentCard.addSubview(submitBtn)
contentCard.addSubview(tipsLab)
navBgView.layoutChain navBgView.layoutChain
.edges(excludingEdge: .bottom) .top()
.heightToWidth(160/375) .edgesHorzontal()
.heightToWidth(253 / 375)
navView.layoutChain navView.layoutChain
.edges(excludingEdge: .bottom) .top()
.edgesHorzontal()
.height(kNaviHeight) .height(kNaviHeight)
titleLab.layoutChain contentCard.layoutChain
.topToBottomOfView(navView, offset: 21) .topToBottomOfView(navBgView, offset: -60)
.edgesHorzontal()
.bottom()
cardTitleLab.layoutChain
.top(17)
.centerX() .centerX()
lineView.layoutChain lineView.layoutChain
.topToBottomOfView(titleLab, offset: 58) .topToBottomOfView(cardTitleLab, offset: 62)
.centerX() .centerX()
.width(10) .width(13)
.height(4) .height(5)
number3.layoutChain number3.layoutChain
.centerY(lineView) .centerY(lineView)
.rightToLeftOfView(lineView, offset: -8) .rightToLeftOfView(lineView, offset: -10)
.width(28) .width(36)
.height(40) .height(52)
number2.layoutChain number2.layoutChain
.topToView(number3) .topToView(number3)
.rightToLeftOfView(number3, offset: -8) .rightToLeftOfView(number3, offset: -10)
.widthToView(number3) .widthToView(number3)
.heightToView(number3) .heightToView(number3)
number1.layoutChain number1.layoutChain
.topToView(number3) .topToView(number3)
.rightToLeftOfView(number2, offset: -8) .rightToLeftOfView(number2, offset: -10)
.widthToView(number3) .widthToView(number3)
.heightToView(number3) .heightToView(number3)
number4.layoutChain number4.layoutChain
.centerY(lineView) .centerY(lineView)
.leftToRightOfView(lineView, offset: 8) .leftToRightOfView(lineView, offset: 10)
.widthToView(number3) .widthToView(number3)
.heightToView(number3) .heightToView(number3)
number5.layoutChain number5.layoutChain
.topToView(number3) .topToView(number3)
.leftToRightOfView(number4, offset: 8) .leftToRightOfView(number4, offset: 10)
.widthToView(number3) .widthToView(number3)
.heightToView(number3) .heightToView(number3)
number6.layoutChain number6.layoutChain
.topToView(number3) .topToView(number3)
.leftToRightOfView(number5, offset: 8) .leftToRightOfView(number5, offset: 10)
.widthToView(number3) .widthToView(number3)
.heightToView(number3) .heightToView(number3)
tipsLab.layoutChain
.topToBottomOfView(lineView, offset: 38)
.centerX()
submitBtn.layoutChain submitBtn.layoutChain
.bottom(kSafeBottomMargin + 36) .topToBottomOfView(number3, offset: 40)
.edgesHorzontal(24)
.height(56)
tipsLab.layoutChain
.topToBottomOfView(submitBtn, offset: 16)
.centerX() .centerX()
.edgesHorzontal(30) .edgesHorzontal(24)
.height(50)
scanBtn.layoutChain
.top(kStatusBarHeight + 6)
.right(18)
.width(32).height(32)
}
private func styleNavIconButton(_ button: UIButton) {
button.backgroundColor = .white
button.cornerRadius = 12
button.clipsToBounds = true
}
private func makeCodeButton() -> UIButton {
let button = UIButton(type: .custom)
button.setTitleColor(.white, for: .selected)
button.setTitleColor(UIColor(hexStr: "#293445"), for: .normal)
button.titleLabel?.font = FontManager.youSheBiaoTiHei(30)
button.setBackgroundColor(UIColor(hexStr: "#16B3FF", alpha: 0.3), for: .normal)
button.setBackgroundColor(UIColor(hexStr: "#5CBBFF"), for: .selected)
button.cornerRadius = 12
button.addTarget(self, action: #selector(numAction), for: .touchUpInside)
return button
} }
lazy var navBgView: UIImageView = { lazy var navBgView: UIImageView = {
let iv = UIImageView() let iv = UIImageView()
iv.image = UIImage(named: "Common/navBar_bg_2") iv.image = UIImage(named: "Group/join_hero_bg")
iv.contentMode = .scaleAspectFill iv.contentMode = .scaleAspectFill
iv.clipsToBounds = true
return iv return iv
}() }()
lazy var contentCard: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 30
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
view.clipsToBounds = true
return view
}()
lazy var navView: BaseNavigationView = { lazy var navView: BaseNavigationView = {
let nav = BaseNavigationView(title: "加入圈子") let nav = BaseNavigationView(title: " ")
nav.addRightButton(scanBtn)
return nav return nav
}() }()
lazy var scanBtn: UIButton = { lazy var scanBtn: UIButton = {
let btn = UIButton(type: .custom) let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "Group/scan"), for: .normal) btn.setBackgroundImage(UIImage(named: "Group/join_scan"), for: .normal)
btn.extendEdgeInsets = UIEdgeInsets(top: 10, left: 20, bottom: 20, right: 18)
return btn return btn
}() }()
lazy var titleLab: UILabel = { lazy var cardTitleLab: UILabel = {
let label = UILabel() let label = UILabel()
label.text = "输入邀请码" label.text = "输入邀请码"
label.font = .systemFont(ofSize: 24, weight: .medium) label.font = .systemFont(ofSize: 16, weight: .bold)
label.textColor = ThemeManager.shared.color.titleAuxColor label.textColor = UIColor(hexStr: "#353B4F")
label.textAlignment = .center label.textAlignment = .center
return label return label
}() }()
lazy var tipsLab: UILabel = { lazy var tipsLab: UILabel = {
let label = UILabel() let label = UILabel()
label.text = "向圈子创建者询问邀请码" label.text = "输入好友分享的邀请码,添加好友专属圈子"
label.font = .systemFont(ofSize: 12, weight: .medium) label.font = .systemFont(ofSize: 14, weight: .medium)
label.textColor = ThemeManager.shared.color.titleAuxColor label.textColor = UIColor(hexStr: "#00ADFE")
label.textAlignment = .center
label.numberOfLines = 0
return label return label
}() }()
lazy var number1: UIButton = { lazy var number1: UIButton = {
let button = UIButton (type: .custom) let button = makeCodeButton()
button.setTitleColor(ThemeManager.shared.color.titleColor, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
button.setBackgroundColor(UIColor(hexStr: "#16B3FF").withAlphaComponent(0.3), for: .normal)
button.setBackgroundColor(UIColor(hexStr: "#5CBBFF"), for: .selected)
button.cornerRadius = 4
button.isSelected = true button.isSelected = true
button.addTarget(self, action: #selector(numAction), for: .touchUpInside)
return button return button
}() }()
lazy var number2: UIButton = { lazy var number2: UIButton = makeCodeButton()
let button = UIButton (type: .custom) lazy var number3: UIButton = makeCodeButton()
button.setTitleColor(ThemeManager.shared.color.titleColor, for: .normal) lazy var number4: UIButton = makeCodeButton()
button.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium) lazy var number5: UIButton = makeCodeButton()
button.setBackgroundColor(UIColor(hexStr: "#16B3FF").withAlphaComponent(0.3), for: .normal) lazy var number6: UIButton = makeCodeButton()
button.setBackgroundColor(UIColor(hexStr: "#5CBBFF"), for: .selected)
button.addTarget(self, action: #selector(numAction), for: .touchUpInside)
button.cornerRadius = 4
return button
}()
lazy var number3: UIButton = {
let button = UIButton (type: .custom)
button.setTitleColor(ThemeManager.shared.color.titleColor, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
button.setBackgroundColor(UIColor(hexStr: "#16B3FF").withAlphaComponent(0.3), for: .normal)
button.setBackgroundColor(UIColor(hexStr: "#5CBBFF"), for: .selected)
button.addTarget(self, action: #selector(numAction), for: .touchUpInside)
button.cornerRadius = 4
return button
}()
lazy var number4: UIButton = {
let button = UIButton (type: .custom)
button.setTitleColor(ThemeManager.shared.color.titleColor, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
button.setBackgroundColor(UIColor(hexStr: "#16B3FF").withAlphaComponent(0.3), for: .normal)
button.setBackgroundColor(UIColor(hexStr: "#5CBBFF"), for: .selected)
button.addTarget(self, action: #selector(numAction), for: .touchUpInside)
button.cornerRadius = 4
return button
}()
lazy var number5: UIButton = {
let button = UIButton (type: .custom)
button.setTitleColor(ThemeManager.shared.color.titleColor, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
button.setBackgroundColor(UIColor(hexStr: "#16B3FF").withAlphaComponent(0.3), for: .normal)
button.setBackgroundColor(UIColor(hexStr: "#5CBBFF"), for: .selected)
button.addTarget(self, action: #selector(numAction), for: .touchUpInside)
button.cornerRadius = 4
return button
}()
lazy var number6: UIButton = {
let button = UIButton (type: .custom)
button.setTitleColor(ThemeManager.shared.color.titleColor, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
button.setBackgroundColor(UIColor(hexStr: "#16B3FF", alpha: 0.3), for: .normal)
button.setBackgroundColor(UIColor(hexStr: "#5CBBFF"), for: .selected)
button.addTarget(self, action: #selector(numAction), for: .touchUpInside)
button.cornerRadius = 4
return button
}()
lazy var lineView: UIView = { lazy var lineView: UIView = {
let view = UIView() let view = UIView()
view.backgroundColor = UIColor(hexStr: "#16B3FF", alpha: 0.3) view.backgroundColor = UIColor(hexStr: "#16B3FF")
view.cornerRadius = 0
return view return view
}() }()
lazy var submitBtn: UIButton = { lazy var submitBtn: UIButton = {
let btn = UIButton(type: .custom) let btn = UIButton(type: .custom)
btn.setTitle("加入", for: .normal) btn.setTitle("加入", for: .normal)
btn.setTitleColor(UIColor(hexStr: "#0F2846"), for: .normal) btn.setTitleColor(.white, for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) btn.titleLabel?.font = FontManager.boboBold(18)
btn.setBackgroundImage(UIImage(named: "Common/gradient_bg"), for: .normal) btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
btn.cornerRadius = 25 btn.cornerRadius = 20
return btn return btn
}() }()
lazy var keyboardConfirmBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("确认", for: .normal)
btn.setTitleColor(.white, for: .normal)
btn.titleLabel?.font = FontManager.boboBold(18)
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
btn.cornerRadius = 20
return btn
}()
private lazy var keyboardConfirmBar: UIView = {
let bar = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 68))
bar.backgroundColor = .white
bar.addSubview(keyboardConfirmBtn)
keyboardConfirmBtn.layoutChain
.edgesHorzontal(16)
.top(8)
.height(52)
return bar
}()
lazy var textField: UITextField = { lazy var textField: UITextField = {
let tf = UITextField() let tf = UITextField()
tf.isHidden = true tf.isHidden = true
tf.keyboardType = .asciiCapable tf.keyboardType = .asciiCapable
tf.autocorrectionType = .no tf.autocorrectionType = .no
tf.returnKeyType = .done tf.returnKeyType = .done
// tf.inputAccessoryView = keyboardConfirmBar
return tf return tf
}() }()

View File

@ -114,8 +114,8 @@ final class CreateBubbleSetupView: UIView {
confirmBtn.setTitleColor(.white, for: .normal) confirmBtn.setTitleColor(.white, for: .normal)
confirmBtn.titleLabel?.font = FontManager.boboBold(18) confirmBtn.titleLabel?.font = FontManager.boboBold(18)
confirmBtn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal) confirmBtn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
confirmBtn.layer.cornerRadius = 16 confirmBtn.cornerRadius = 20
confirmBtn.layoutChain.left(20).right(20).bottom(34).height(56) confirmBtn.layoutChain.left(30).right(30).bottom(34).height(56)
} }
private func setupPicker() { private func setupPicker() {

View File

@ -0,0 +1,103 @@
//
// LockDistractVC.swift
// QuickLocation
//
import UIKit
import RxSwift
import RxCocoa
final class LockDistractVC: BaseViewController {
private var rootView: LockDistractView!
private var groupModel: GroupModel?
private var members: [GroupMemberModel] = []
private var selectedIndex = 0
override var isNavigationBarHidden: Bool { true }
override func loadView() {
rootView = LockDistractView(frame: UIScreen.main.bounds)
view = rootView
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(hexStr: "#FAFAFA")
bind()
loadGroupMembers()
}
private func bind() {
rootView.navView.onBackTapped = { [weak self] in
self?.navigationController?.popViewController(animated: true)
}
rootView.switchGroupBtn.rx.tap
.subscribe(onNext: { [weak self] in
self?.switchGroup()
})
.disposed(by: disposeBag)
rootView.memberPrevBtn.rx.tap
.subscribe(onNext: { [weak self] in
self?.stepMember(-1)
})
.disposed(by: disposeBag)
rootView.memberNextBtn.rx.tap
.subscribe(onNext: { [weak self] in
self?.stepMember(1)
})
.disposed(by: disposeBag)
// /
rootView.notFetchedView.rx.tapGesture
.subscribe(onNext: { _ in
/// TODO
}).disposed(by: disposeBag)
}
private func loadGroupMembers() {
GroupService.groupInfo().subscribe(onNext: { [weak self] response in
guard let self, let model = response.model else { return }
self.groupModel = model
self.members = model.select_group_employee
self.selectedIndex = 0
self.refreshMember()
self.rootView.setTodayLockCount(0)
}).disposed(by: disposeBag)
}
private func stepMember(_ delta: Int) {
guard !members.isEmpty else { return }
let count = members.count
selectedIndex = (selectedIndex + delta + count) % count
refreshMember()
}
private func refreshMember() {
guard members.indices.contains(selectedIndex) else {
rootView.configureMember(name: " ", avatar: nil)
return
}
let m = members[selectedIndex]
rootView.configureMember(name: m.nick_name, avatar: m.userIcon)
}
private func switchGroup() {
guard let groupModel else {
loadGroupMembers()
return
}
GroupListPopView.show(groupModel: groupModel) { [weak self] groupKey in
guard let self, let key = groupKey else { return }
GroupService.operate(opType: "setdefault", requestData: ["group_key": key])
.subscribe(onNext: { [weak self] _ in
NotificationCenter.default.post(name: .RefreshGroupInfoNotification, object: nil)
self?.loadGroupMembers()
})
.disposed(by: self.disposeBag)
}
}
}

View File

@ -0,0 +1,605 @@
//
// LockDistractView.swift
// QuickLocation
//
import UIKit
import RxSwift
import RxCocoa
final class LockDistractView: UIView {
var disposeBag = DisposeBag()
let wallpaperNames = [
"LockDistract/wallpaper_1",
"LockDistract/wallpaper_2",
"LockDistract/wallpaper_3"
]
private(set) var selectedWallpaperIndex = 0
private let copyMaxLength = 20
// MARK: - Init
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor(hexStr: "#FAFAFA")
setupUI()
setupCopyLimit()
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
// MARK: - Public
func configureMember(name: String, avatar: UIImage?) {
memberNameLab.text = name.isEmpty ? " " : name
memberAvatar.image = avatar ?? UIImage(named: "Common/default_avatar")
}
func setTodayLockCount(_ count: Int) {
todayCountLab.text = "\(count)"
}
// MARK: - UI
private func setupUI() {
addSubview(navBgView)
addSubview(navView)
navView.addRightButton(switchGroupBtn)
addSubview(scrollView)
scrollView.addSubview(contentView)
addSubview(lockBtn)
contentView.addSubview(titleLab)
contentView.addSubview(subtitleLab)
contentView.addSubview(heroPlaceholder)
contentView.addSubview(statsRow)
statsRow.addArrangedSubview(memberCard)
statsRow.addArrangedSubview(todayCard)
contentView.addSubview(appsSectionTitle)
contentView.addSubview(appsCard)
appsCard.addSubview(appsHeaderRow)
appsCard.addSubview(unknownAppIcon)
contentView.addSubview(styleSectionTitle)
contentView.addSubview(styleCard)
addSubview(tipRow)
navBgView.layoutChain
.edges(excludingEdge: .bottom)
.heightToWidth(160/375)
navView.layoutChain
.edges(excludingEdge: .bottom)
.height(kNaviHeight)
lockBtn.layoutChain
.bottom(kSafeBottomMargin + 0)
.edgesHorzontal(30)
.height(56)
scrollView.layoutChain
.topToBottomOfView(navView)
.edgesHorzontal()
.bottomToTopOfView(tipRow, offset: -12)
contentView.layoutChain
.edges()
.widthToView(scrollView)
titleLab.layoutChain
.top(8)
.left(20)
subtitleLab.layoutChain
.topToBottomOfView(titleLab, offset: 6)
.left(20)
.right(140)
heroPlaceholder.layoutChain
.top(0)
.right(16)
.width(120)
.height(100)
statsRow.layoutChain
.topToBottomOfView(subtitleLab, offset: 20)
.edgesHorzontal(16)
.height(120)
appsSectionTitle.layoutChain
.topToBottomOfView(statsRow, offset: 22)
.left(20)
appsCard.layoutChain
.topToBottomOfView(appsSectionTitle, offset: 12)
.edgesHorzontal(16)
appsHeaderRow.layoutChain
.top(16)
.edgesHorzontal(14)
unknownAppIcon.layoutChain
.topToBottomOfView(appsHeaderRow, offset: 14)
.left(14)
.width(56)
.height(56)
.bottom(16)
styleSectionTitle.layoutChain
.topToBottomOfView(appsCard, offset: 22)
.left(20)
styleCard.layoutChain
.topToBottomOfView(styleSectionTitle, offset: 12)
.edgesHorzontal(16)
.bottom(20)
tipRow.layoutChain
.centerX()
.bottomToTopOfView(lockBtn, offset: -6)
}
private func setupCopyLimit() {
copyTF.rx.text.orEmpty
.subscribe(onNext: { [weak self] text in
guard let self else { return }
if text.count > self.copyMaxLength {
self.copyTF.text = String(text.prefix(self.copyMaxLength))
}
let count = self.copyTF.text?.count ?? 0
self.copyCountLab.text = "\(count)/\(self.copyMaxLength)"
})
.disposed(by: disposeBag)
}
// MARK: - Views
lazy var navBgView: UIImageView = {
let iv = UIImageView()
iv.image = UIImage(named: "Common/navBar_bg_2")
iv.contentMode = .scaleAspectFill
return iv
}()
lazy var navView: BaseNavigationView = {
BaseNavigationView(title: "")
}()
lazy var switchGroupBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.backgroundColor = .white.withAlphaComponent(0.9)
btn.setTitle(" 切换圈子 ", for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
btn.setTitleColor(UIColor(hexStr: "#293445"), for: .normal)
btn.cornerRadius = 8
btn.extendEdgeInsets = UIEdgeInsets(top: 20, left: 12, bottom: 12, right: 4)
return btn
}()
lazy var scrollView: UIScrollView = {
let sv = UIScrollView()
sv.showsVerticalScrollIndicator = false
sv.alwaysBounceVertical = true
sv.keyboardDismissMode = .onDrag
return sv
}()
lazy var contentView = UIView()
lazy var titleLab: UILabel = {
let lab = UILabel()
lab.text = "锁住分心"
lab.font = FontManager.boboBold(30)
lab.textColor = UIColor(hexStr: "#293445")
return lab
}()
lazy var subtitleLab: UILabel = {
let lab = UILabel()
lab.text = "减少干扰,只专注重要的事"
lab.font = .systemFont(ofSize: 13, weight: .medium)
lab.textColor = UIColor(hexStr: "#8A94A6")
lab.numberOfLines = 2
return lab
}()
lazy var heroPlaceholder: UIView = {
let view = UIView()
view.backgroundColor = .clear
return view
}()
lazy var statsRow: UIStackView = {
let stack = UIStackView()
stack.axis = .horizontal
stack.spacing = 12
stack.distribution = .fillEqually
return stack
}()
lazy var memberCard: UIView = {
let view = UIView()
view.backgroundColor = .white
view.cornerRadius = 16
view.addSubview(memberPrevBtn)
view.addSubview(memberAvatar)
view.addSubview(memberNextBtn)
view.addSubview(memberNameChip)
memberNameChip.addSubview(memberNameLab)
memberAvatar.layoutChain
.top(18)
.centerX()
.width(56)
.height(56)
memberPrevBtn.layoutChain
.centerY(memberAvatar)
.left(8)
.width(28)
.height(28)
memberNextBtn.layoutChain
.centerY(memberAvatar)
.right(8)
.width(28)
.height(28)
memberNameChip.layoutChain
.topToBottomOfView(memberAvatar, offset: 10)
.centerX()
.left(8, relation: .greaterThanOrEqual)
.right(8, relation: .greaterThanOrEqual)
.bottom(14)
memberNameLab.layoutChain
.top(6)
.bottom(6)
.left(5)
.right(5)
return view
}()
lazy var memberAvatar: UIImageView = {
let iv = UIImageView(image: UIImage(named: "Common/default_avatar"))
iv.contentMode = .scaleAspectFill
iv.clipsToBounds = true
iv.cornerRadius = 16
return iv
}()
lazy var memberNameChip: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#F2F2F2")
view.cornerRadius = 8
return view
}()
lazy var memberNameLab: UILabel = {
let lab = UILabel()
lab.text = " "
lab.font = .systemFont(ofSize: 10, weight: .bold)
lab.textColor = UIColor(hexStr: "#293445")
lab.textAlignment = .center
lab.lineBreakMode = .byTruncatingTail
return lab
}()
lazy var memberPrevBtn: UIButton = {
let btn = UIButton(type: .system)
btn.setImage(UIImage(systemName: "chevron.left"), for: .normal)
btn.tintColor = UIColor(hexStr: "#9CA3AF")
return btn
}()
lazy var memberNextBtn: UIButton = {
let btn = UIButton(type: .system)
btn.setImage(UIImage(systemName: "chevron.right"), for: .normal)
btn.tintColor = UIColor(hexStr: "#9CA3AF")
return btn
}()
lazy var todayCard: UIView = {
let view = UIView()
view.backgroundColor = .white
view.cornerRadius = 16
let title = UILabel()
title.text = "今日锁定应用"
title.font = .systemFont(ofSize: 16, weight: .bold)
title.textColor = UIColor(hexStr: "#293445")
view.addSubview(title)
title.layoutChain.top(21).left(23)
view.addSubview(todayCountLab)
view.addSubview(todayUnitLab)
todayCountLab.layoutChain
.leftToView(title, offset: 4)
.bottom(10)
todayUnitLab.layoutChain
.leftToRightOfView(todayCountLab, offset: 7)
.bottomToView(todayCountLab, offset: -12)
return view
}()
lazy var todayCountLab: UILabel = {
let lab = UILabel()
lab.text = "0"
lab.font = .systemFont(ofSize: 50, weight: .heavy)
lab.textColor = UIColor(hexStr: "#00ADFE")
return lab
}()
lazy var todayUnitLab: UILabel = {
let lab = UILabel()
lab.text = ""
lab.font = .systemFont(ofSize: 16, weight: .bold)
lab.textColor = UIColor(hexStr: "#293445")
return lab
}()
lazy var appsSectionTitle: UIView = {
makeSectionTitle("锁定应用")
}()
lazy var appsCard: UIView = {
let view = UIView()
view.backgroundColor = .white
view.cornerRadius = 26
return view
}()
lazy var appsHeaderRow: UIView = {
let row = UIView()
let left = UILabel()
left.text = "选择锁定TA的app"
left.font = .systemFont(ofSize: 14, weight: .medium)
left.textColor = UIColor(hexStr: "#293445")
row.addSubview(left)
left.layoutChain.left().centerY()
row.addSubview(notFetchedView)
notFetchedView.layoutChain.right().centerY().height(28)
row.layoutChain.height(28)
return row
}()
lazy var notFetchedView: UIView = {
let view = UIView()
let warn = UIView()
warn.backgroundColor = UIColor(hexStr: "#FA5151")
warn.cornerRadius = 5
let warnText = UILabel()
warnText.text = "!"
warnText.textColor = .white
warnText.font = .systemFont(ofSize: 8)
view.addSubview(warn)
warn.addSubview(warnText)
warnText.layoutChain.centerX().centerY()
let notFetchedText = UILabel()
notFetchedText.text = "未获取"
notFetchedText.textColor = UIColor(hexStr: "#293445")
notFetchedText.font = .systemFont(ofSize: 12, weight: .medium)
view.addSubview(notFetchedText)
let arrow = UIImageView(image: UIImage(named: "LockDistract/arrow_right"))
view.addSubview(arrow)
view.addSubview(notFetchedText)
arrow.layoutChain
.right(14)
.width(10)
.heightToWidth(1)
.centerY()
notFetchedText.layoutChain
.rightToLeftOfView(arrow)
.centerY()
warn.layoutChain
.rightToLeftOfView(notFetchedText, offset: -4)
.centerY()
.width(10).height(10)
return view
}()
lazy var unknownAppIcon: UIImageView = {
let iv = UIImageView(image: UIImage(named: "LockDistract/app_unknown"))
iv.contentMode = .scaleAspectFit
return iv
}()
lazy var styleSectionTitle: UIView = {
makeSectionTitle("锁定界面样式")
}()
lazy var styleCard: UIView = {
let card = UIView()
card.backgroundColor = .white
card.cornerRadius = 26
let wallLab = UILabel()
wallLab.text = "锁定壁纸图片"
wallLab.font = .systemFont(ofSize: 13, weight: .medium)
wallLab.textColor = UIColor(hexStr: "#8A94A6")
card.addSubview(wallLab)
wallLab.layoutChain.top(16).left(14)
card.addSubview(wallpaperCV)
wallpaperCV.layoutChain
.topToBottomOfView(wallLab, offset: 10)
.edgesHorzontal(14)
.height(74)
let copyLab = UILabel()
copyLab.text = "锁定文案"
copyLab.font = .systemFont(ofSize: 13, weight: .medium)
copyLab.textColor = UIColor(hexStr: "#8A94A6")
card.addSubview(copyLab)
copyLab.layoutChain.topToBottomOfView(wallpaperCV, offset: 16).left(14)
let input = UIView()
input.backgroundColor = UIColor(hexStr: "#F5F6F8")
input.cornerRadius = 12
card.addSubview(input)
input.layoutChain
.topToBottomOfView(copyLab, offset: 10)
.edgesHorzontal(14)
.height(44)
.bottom(16)
input.addSubview(copyTF)
input.addSubview(copyCountLab)
copyCountLab.layoutChain.centerY().right(12).width(35)
copyTF.layoutChain.edgesVertical().left(12).rightToLeftOfView(copyCountLab, offset: -8)
return card
}()
lazy var wallpaperCV: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.itemSize = CGSize(width: 50, height: 74)
layout.minimumLineSpacing = 12
layout.minimumInteritemSpacing = 0
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .clear
cv.showsHorizontalScrollIndicator = false
cv.register(LockDistractWallpaperCell.self)
cv.dataSource = self
cv.delegate = self
return cv
}()
lazy var copyTF: UITextField = {
let tf = UITextField()
tf.font = .systemFont(ofSize: 14, weight: .medium)
tf.textColor = UIColor(hexStr: "#293445")
tf.placeholderColor(placeholder: "随便说点什么吧~",
color: UIColor(hexStr: "#999999"),
font: .systemFont(ofSize: 14, weight: .medium))
return tf
}()
lazy var copyCountLab: UILabel = {
let lab = UILabel()
lab.text = "0/20"
lab.font = .systemFont(ofSize: 14, weight: .medium)
lab.textColor = UIColor(hexStr: "#AAAAAA")
return lab
}()
lazy var tipRow: UIView = {
let row = UIView()
let icon = UIImageView(image: UIImage(systemName: "info.circle.fill"))
icon.tintColor = UIColor(hexStr: "#16B3FF")
let lab = UILabel()
lab.text = "锁定功能只有在圈主的情况下才能使用哟~"
lab.font = .systemFont(ofSize: 12, weight: .regular)
lab.textColor = UIColor(hexStr: "#00ADFE")
lab.numberOfLines = 0
row.addSubview(icon)
row.addSubview(lab)
icon.layoutChain.left().top(1).width(14).height(14)
lab.layoutChain.leftToRightOfView(icon, offset: 6).right().top().bottom()
return row
}()
lazy var lockBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("锁定", for: .normal)
btn.setTitleColor(.white, for: .normal)
btn.titleLabel?.font = FontManager.boboBold(18)
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
btn.cornerRadius = 20
return btn
}()
private func makeSectionTitle(_ text: String) -> UIView {
let row = UIView()
let star = UIImageView(image: UIImage(named: "LockDistract/section_star"))
// SF Symbol LockDistract/section_star
// let config = UIImage.SymbolConfiguration(pointSize: 12, weight: .bold)
// star.image = UIImage(systemName: "sparkle", withConfiguration: config)
// star.tintColor = UIColor(hexStr: "#16B3FF")
// star.contentMode = .scaleAspectFit
let lab = UILabel()
lab.text = text
lab.font = .systemFont(ofSize: 16, weight: .bold)
lab.textColor = UIColor(hexStr: "#1F2A44")
row.addSubview(star)
row.addSubview(lab)
star.layoutChain.left().centerY().width(14).height(14)
lab.layoutChain.leftToRightOfView(star, offset: 6).right().centerY()
row.layoutChain.height(22)
return row
}
}
extension LockDistractView: UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
wallpaperNames.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(for: indexPath) as LockDistractWallpaperCell
cell.configure(imageName: wallpaperNames[indexPath.item],
isSelected: indexPath.item == selectedWallpaperIndex)
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedWallpaperIndex = indexPath.item
collectionView.reloadData()
}
}
final class LockDistractWallpaperCell: UICollectionViewCell {
private let frameView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#F6F6F6")
view.cornerRadius = 14
return view
}()
private let imageView: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleAspectFill
iv.clipsToBounds = true
iv.cornerRadius = 10
return iv
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(frameView)
frameView.addSubview(imageView)
frameView.layoutChain.edges()
imageView.layoutChain
.centerY()
.centerX()
.width(40)
.height(40)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
func configure(imageName: String, isSelected: Bool) {
imageView.image = UIImage(named: imageName)
frameView.backgroundColor = UIColor(hexStr: isSelected ? "#EAF8FF" : "#F6F6F6")
frameView.borderWidth = isSelected ? 1 : 0
frameView.borderColor = UIColor(hexStr: isSelected ? "#00ADFE" : "")
}
}

View File

@ -11,8 +11,13 @@ import RxCocoa
import RxDataSources import RxDataSources
struct PrivacyPolicyItem { struct PrivacyPolicyItem {
enum Destination {
case web(String)
case appRestrict
}
let name: String let name: String
let url: String let destination: Destination
} }
class PrivacyPolicyVC: BaseViewController { class PrivacyPolicyVC: BaseViewController {
@ -25,9 +30,10 @@ class PrivacyPolicyVC: BaseViewController {
} }
private let list: [PrivacyPolicyItem] = [ private let list: [PrivacyPolicyItem] = [
PrivacyPolicyItem(name: "用户协议", url: URLManager.shared.userAgreementUrl), PrivacyPolicyItem(name: "用户协议", destination: .web(URLManager.shared.userAgreementUrl)),
PrivacyPolicyItem(name: "隐私政策", url: URLManager.shared.privacyPolicyUrl), PrivacyPolicyItem(name: "隐私政策", destination: .web(URLManager.shared.privacyPolicyUrl)),
PrivacyPolicyItem(name: "儿童隐私政策", url: URLManager.shared.kidsPrivacyUrl) PrivacyPolicyItem(name: "儿童隐私政策", destination: .web(URLManager.shared.kidsPrivacyUrl)),
PrivacyPolicyItem(name: "应用配对库", destination: .appRestrict)
] ]
override func viewDidLoad() { override func viewDidLoad() {
@ -50,7 +56,12 @@ class PrivacyPolicyVC: BaseViewController {
// //
rootView.tableView.rx.modelSelected(PrivacyPolicyItem.self) rootView.tableView.rx.modelSelected(PrivacyPolicyItem.self)
.subscribe(onNext: { item in .subscribe(onNext: { item in
AppRouter.push(Route.web, userInfo: ["url": item.url]) switch item.destination {
case .web(let url):
AppRouter.push(Route.web, userInfo: ["url": url])
case .appRestrict:
AppRouter.push(Route.appRestrict)
}
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>ShieldConfiguration</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.ManagedSettingsUI.shield-configuration-service</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).ShieldConfigurationExtension</string>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.family-controls</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.cn.zuomeng.jisuloca</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,38 @@
import ManagedSettings
import ManagedSettingsUI
import UIKit
final class ShieldConfigurationExtension: ShieldConfigurationDataSource {
override func configuration(shielding application: Application) -> ShieldConfiguration {
makeConfiguration()
}
override func configuration(shielding application: Application, in category: ActivityCategory) -> ShieldConfiguration {
makeConfiguration()
}
override func configuration(shielding webDomain: WebDomain) -> ShieldConfiguration {
makeConfiguration()
}
override func configuration(shielding webDomain: WebDomain, in category: ActivityCategory) -> ShieldConfiguration {
makeConfiguration()
}
private func makeConfiguration() -> ShieldConfiguration {
let config = AppRestrictSharedStore.shieldConfig
let image = AppRestrictSharedStore.loadShieldImage()
let background = UIColor(red: 0.98, green: 0.98, blue: 0.98, alpha: 1)
let titleColor = UIColor(red: 0.16, green: 0.20, blue: 0.27, alpha: 1)
let bodyColor = UIColor(red: 0.46, green: 0.46, blue: 0.46, alpha: 1)
return ShieldConfiguration(
backgroundBlurStyle: .systemMaterial,
backgroundColor: background,
icon: image,
title: ShieldConfiguration.Label(text: config.title, color: titleColor),
subtitle: ShieldConfiguration.Label(text: config.subtitle, color: bodyColor),
primaryButtonLabel: ShieldConfiguration.Label(text: config.primaryButtonLabel, color: .white),
primaryButtonBackgroundColor: UIColor(red: 0.09, green: 0.70, blue: 1.0, alpha: 1)
)
}
}

View File

@ -0,0 +1,152 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'xcodeproj'
ROOT = File.expand_path('..', __dir__)
PROJECT_PATH = File.join(ROOT, 'QuickLocation.xcodeproj')
project = Xcodeproj::Project.open(PROJECT_PATH)
main_target = project.targets.find { |t| t.name == 'QuickLocation' }
raise 'QuickLocation target not found' unless main_target
def ensure_group(project, path_components)
group = project.main_group
path_components.each do |name|
next_group = group.children.find { |c| c.respond_to?(:name) && (c.name == name || c.path == name) }
group = next_group || group.new_group(name)
end
group
end
def add_file(project, target, group, relative_path, root:, resources: false)
abs = File.join(root, relative_path)
raise "Missing file: #{abs}" unless File.exist?(abs)
ref = group.files.find { |f| f.real_path.to_s == abs || f.path == File.basename(relative_path) }
ref ||= group.new_file(abs)
if resources
unless target.resources_build_phase.files_references.include?(ref)
target.add_resources([ref])
end
else
unless target.source_build_phase.files_references.include?(ref)
target.source_build_phase.add_file_reference(ref)
end
end
ref
end
def configure_extension_target(target, bundle_id, entitlements, info_plist)
target.build_configurations.each do |config|
bs = config.build_settings
bs['PRODUCT_BUNDLE_IDENTIFIER'] = bundle_id
bs['PRODUCT_NAME'] = target.name
bs['PRODUCT_MODULE_NAME'] = target.name
bs['CODE_SIGN_ENTITLEMENTS'] = entitlements
bs['INFOPLIST_FILE'] = info_plist
bs['GENERATE_INFOPLIST_FILE'] = 'NO'
bs['IPHONEOS_DEPLOYMENT_TARGET'] = '16.0'
bs['TARGETED_DEVICE_FAMILY'] = '1'
bs['SKIP_INSTALL'] = 'YES'
bs['LD_RUNPATH_SEARCH_PATHS'] = '$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks'
bs['DEVELOPMENT_TEAM'] = 'LRDLWZ2Y83'
bs['SWIFT_DEFAULT_ACTOR_ISOLATION'] = 'nonisolated'
bs['SWIFT_VERSION'] = '5.0'
bs['CODE_SIGN_STYLE'] = 'Automatic'
bs['CURRENT_PROJECT_VERSION'] = '1'
bs['MARKETING_VERSION'] = '1.0'
end
end
def find_or_create_extension(project, name)
existing = project.targets.find { |t| t.name == name }
return existing if existing
project.new_target(:app_extension, name, :ios, '15.0')
end
def link_system_framework(target, name)
frameworks = project = target.project
frameworks_group = project.frameworks_group
path = "System/Library/Frameworks/#{name}.framework"
ref = frameworks_group.files.find { |f| f.path == "#{name}.framework" || f.name == "#{name}.framework" }
ref ||= frameworks_group.new_file(path)
ref.name = "#{name}.framework"
ref.source_tree = 'SDKROOT'
unless target.frameworks_build_phase.files_references.include?(ref)
target.frameworks_build_phase.add_file_reference(ref)
end
end
# Shared
shared_group = ensure_group(project, ['AppRestrictShared'])
add_file(project, main_target, shared_group, 'AppRestrictShared/AppRestrictShared.swift', root: ROOT)
# Main app
mgr_group = ensure_group(project, ['QuickLocation', 'Manager', 'AppRestrict'])
add_file(project, main_target, mgr_group, 'QuickLocation/Manager/AppRestrict/AppRestrictManager.swift', root: ROOT)
section_group = ensure_group(project, ['QuickLocation', 'Section', 'AppRestrict'])
%w[
AppCatalogStore.swift
AppRestrictCell.swift
AppRestrictVC.swift
AppRestrictView.swift
AppRestrictShieldSettingsVC.swift
FamilyActivityPickerHost.swift
SelectActivityVC.swift
].each do |name|
add_file(project, main_target, section_group, "QuickLocation/Section/AppRestrict/#{name}", root: ROOT)
end
add_file(project, main_target, section_group, 'QuickLocation/Section/AppRestrict/app_catalog.json', root: ROOT, resources: true)
%w[FamilyControls ManagedSettings DeviceActivity ManagedSettingsUI].each do |fw|
link_system_framework(main_target, fw)
end
# Monitor extension
monitor = find_or_create_extension(project, 'DeviceActivityMonitorExtension')
monitor_group = ensure_group(project, ['DeviceActivityMonitorExtension'])
add_file(project, monitor, monitor_group, 'DeviceActivityMonitorExtension/DeviceActivityMonitorExtension.swift', root: ROOT)
add_file(project, monitor, shared_group, 'AppRestrictShared/AppRestrictShared.swift', root: ROOT)
configure_extension_target(
monitor,
'cn.zuomeng.jisuloca.DeviceActivityMonitor',
'DeviceActivityMonitorExtension/DeviceActivityMonitorExtension.entitlements',
'DeviceActivityMonitorExtension/Info.plist'
)
%w[DeviceActivity FamilyControls ManagedSettings].each { |fw| link_system_framework(monitor, fw) }
# Shield extension
shield = find_or_create_extension(project, 'ShieldConfigurationExtension')
shield_group = ensure_group(project, ['ShieldConfigurationExtension'])
add_file(project, shield, shield_group, 'ShieldConfigurationExtension/ShieldConfigurationExtension.swift', root: ROOT)
add_file(project, shield, shared_group, 'AppRestrictShared/AppRestrictShared.swift', root: ROOT)
configure_extension_target(
shield,
'cn.zuomeng.jisuloca.ShieldConfiguration',
'ShieldConfigurationExtension/ShieldConfigurationExtension.entitlements',
'ShieldConfigurationExtension/Info.plist'
)
%w[ManagedSettings ManagedSettingsUI FamilyControls UIKit].each { |fw| link_system_framework(shield, fw) }
# Embed
embed_phase = main_target.copy_files_build_phases.find { |p| p.dst_subfolder_spec == '13' || p.name == 'Embed Foundation Extensions' }
unless embed_phase
embed_phase = main_target.new_copy_files_build_phase('Embed Foundation Extensions')
embed_phase.symbol_dst_subfolder_spec = :plug_ins
end
[monitor, shield].each do |ext|
main_target.add_dependency(ext) unless main_target.dependencies.any? { |d| d.target == ext }
product = ext.product_reference
unless embed_phase.files.any? { |f| f.file_ref == product }
bf = embed_phase.add_file_reference(product)
bf.settings = { 'ATTRIBUTES' => ['RemoveHeadersOnCopy'] }
end
end
project.save
puts 'OK: App Restrict targets wired into QuickLocation.xcodeproj'

View File

@ -0,0 +1,26 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'xcodeproj'
ROOT = File.expand_path('..', __dir__)
PROJECT_PATH = File.join(ROOT, 'QuickLocation.xcodeproj')
project = Xcodeproj::Project.open(PROJECT_PATH)
main_target = project.targets.find { |t| t.name == 'QuickLocation' }
raise 'QuickLocation target not found' unless main_target
section_group = project.main_group.find_subpath('QuickLocation/Section', true)
lock_group = section_group['LockDistract'] || section_group.new_group('LockDistract', 'LockDistract')
%w[LockDistractView.swift LockDistractVC.swift].each do |name|
abs = File.join(ROOT, 'QuickLocation/Section/LockDistract', name)
raise "Missing #{abs}" unless File.exist?(abs)
ref = lock_group.files.find { |f| f.path == name || f.real_path.to_s == abs }
ref ||= lock_group.new_file(abs)
unless main_target.source_build_phase.files_references.include?(ref)
main_target.source_build_phase.add_file_reference(ref)
end
end
project.save
puts 'Added LockDistract sources to QuickLocation target'