diff --git a/.gitignore b/.gitignore index 73d65739..5bb2abc2 100644 --- a/.gitignore +++ b/.gitignore @@ -47,4 +47,5 @@ npm-debug.log screenshot/ .opencode/ .cursor/ +.codex/ openspec/ diff --git a/AppRestrictShared/AppRestrictShared.swift b/AppRestrictShared/AppRestrictShared.swift new file mode 100644 index 00000000..d545b0c3 --- /dev/null +++ b/AppRestrictShared/AppRestrictShared.swift @@ -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) -> Data? { + let datas = tokens.compactMap { encode($0) } + return try? PropertyListEncoder().encode(datas) + } + + static func decodeTokenSet(_ data: Data) -> Set { + 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 { + 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) { + 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)) + } + } +} diff --git a/DeviceActivityMonitorExtension/DeviceActivityMonitorExtension.entitlements b/DeviceActivityMonitorExtension/DeviceActivityMonitorExtension.entitlements new file mode 100644 index 00000000..4a8cc927 --- /dev/null +++ b/DeviceActivityMonitorExtension/DeviceActivityMonitorExtension.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.developer.family-controls + + com.apple.security.application-groups + + group.cn.zuomeng.jisuloca + + + diff --git a/DeviceActivityMonitorExtension/DeviceActivityMonitorExtension.swift b/DeviceActivityMonitorExtension/DeviceActivityMonitorExtension.swift new file mode 100644 index 00000000..7aae6493 --- /dev/null +++ b/DeviceActivityMonitorExtension/DeviceActivityMonitorExtension.swift @@ -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) + } +} diff --git a/DeviceActivityMonitorExtension/Info.plist b/DeviceActivityMonitorExtension/Info.plist new file mode 100644 index 00000000..d68c75b6 --- /dev/null +++ b/DeviceActivityMonitorExtension/Info.plist @@ -0,0 +1,27 @@ + + + + + CFBundleDisplayName + DeviceActivityMonitor + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSExtension + + NSExtensionPointIdentifier + com.apple.deviceactivity.monitor-extension + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).DeviceActivityMonitorExtension + + + diff --git a/Podfile b/Podfile index d9975def..84e322ee 100644 --- a/Podfile +++ b/Podfile @@ -1,6 +1,6 @@ # Uncomment the next line to define a global platform for your project source 'https://gitee.com/mirrors/CocoaPods-Specs.git' -platform :ios, '15.0' +platform :ios, '16.0' use_frameworks! target 'QuickLocation' do # Comment the next line if you don't want to use dynamic frameworks diff --git a/QuickLocation.xcodeproj/project.pbxproj b/QuickLocation.xcodeproj/project.pbxproj index e5da6482..0a1665ab 100644 --- a/QuickLocation.xcodeproj/project.pbxproj +++ b/QuickLocation.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* 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 */; }; 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 */; }; @@ -47,7 +50,6 @@ 305A76AD2FCA8C7000227D26 /* UIApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74F62FCA8C7000227D26 /* UIApplicationExtension.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 */; }; - 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 */; }; 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 */; }; @@ -187,11 +189,6 @@ 30A87A642FEE75520095E7C6 /* CreateBubbleTipsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A632FEE75520095E7C6 /* CreateBubbleTipsView.swift */; }; 30A87A662FEE843E0095E7C6 /* CreateBubbleDoneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A652FEE843E0095E7C6 /* CreateBubbleDoneView.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 */; }; 30A87A6D2FEF5BA10095E7C6 /* SearchLocationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A6C2FEF5BA10095E7C6 /* SearchLocationVC.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 */; }; 30D74AAE2FEA13E00050EB2C /* ScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AAD2FEA13E00050EB2C /* ScheduleView.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 */; }; 30D74AB42FEA25B90050EB2C /* ViewedModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AB32FEA25B90050EB2C /* ViewedModel.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 */; }; 30EFF3E72FDAA93D00EB35D4 /* PrivacyPolicyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF3E62FDAA93D00EB35D4 /* PrivacyPolicyView.swift */; }; 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 */; }; 55B2179130217D6600784774 /* HomeView2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B2179030217D6600784774 /* HomeView2.swift */; }; 55B2179330218CE100784774 /* GroupMemberView2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B2179230218CE100784774 /* GroupMemberView2.swift */; }; 55B217953022DD6B00784774 /* 荆南波波黑-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 55B217943022DD6B00784774 /* 荆南波波黑-Bold.ttf */; }; 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 */; }; 55B217C13022F0A000784721 /* MinePhotoWallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217C03022F0A000784720 /* MinePhotoWallView.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 */; }; 55B218A13024A0010078470A /* MemberInfoVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784709 /* MemberInfoVC.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 */; }; 55BF752D2FFE53F70055DA57 /* LocationPermissionPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.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 */; }; + 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 */; }; + 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 */ +/* 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 */ + 08002267FC4390B20D3510B3 /* ShieldConfigurationExtension.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ShieldConfigurationExtension.swift; path = ShieldConfigurationExtension/ShieldConfigurationExtension.swift; sourceTree = ""; }; + 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 = ""; }; + 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 = ""; }; 305A74C72FCA8C7000227D26 /* Observable+Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Observable+Response.swift"; sourceTree = ""; }; 305A74C82FCA8C7000227D26 /* Single+Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Single+Response.swift"; sourceTree = ""; }; @@ -372,7 +443,6 @@ 305A74F62FCA8C7000227D26 /* UIApplicationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationExtension.swift; sourceTree = ""; }; 305A74F72FCA8C7000227D26 /* UIButton+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+Extension.swift"; sourceTree = ""; }; 305A74F82FCA8C7000227D26 /* UIColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extension.swift"; sourceTree = ""; }; - 55B217A33022F0B100784707 /* UIDevice+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Extension.swift"; sourceTree = ""; }; 305A74F92FCA8C7000227D26 /* UIFont+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Extension.swift"; sourceTree = ""; }; 305A74FA2FCA8C7000227D26 /* UIImage+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Extension.swift"; sourceTree = ""; }; 305A74FB2FCA8C7000227D26 /* UIImage+Resource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Resource.swift"; sourceTree = ""; }; @@ -518,11 +588,6 @@ 30A87A632FEE75520095E7C6 /* CreateBubbleTipsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleTipsView.swift; sourceTree = ""; }; 30A87A652FEE843E0095E7C6 /* CreateBubbleDoneView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleDoneView.swift; sourceTree = ""; }; 30A87A672FEE86560095E7C6 /* CreateBubblePopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubblePopView.swift; sourceTree = ""; }; - 55B219C13024C00100784721 /* BubbleHeroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleHeroView.swift; sourceTree = ""; }; - 55B219C13024C00100784723 /* CreateBubbleSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleSetupView.swift; sourceTree = ""; }; - 55B219C13024C00100784725 /* BubbleKnowledgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleKnowledgeView.swift; sourceTree = ""; }; - 55B219C13024C00100784727 /* BubbleKnowledgeVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleKnowledgeVC.swift; sourceTree = ""; }; - 55C219C13024C00100784730 /* SearchLocationHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationHeaderView.swift; sourceTree = ""; }; 30A87A6A2FEF5B950095E7C6 /* SearchLocationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationView.swift; sourceTree = ""; }; 30A87A6C2FEF5BA10095E7C6 /* SearchLocationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationVC.swift; sourceTree = ""; }; 30A87A6E2FEF7BE40095E7C6 /* SearchLocationResultVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationResultVC.swift; sourceTree = ""; }; @@ -571,8 +636,6 @@ 30D74AAA2FE8C7700050EB2C /* GPSSignalHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GPSSignalHelper.swift; sourceTree = ""; }; 30D74AAD2FEA13E00050EB2C /* ScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleView.swift; sourceTree = ""; }; 30D74AAF2FEA13ED0050EB2C /* ScheduleVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleVC.swift; sourceTree = ""; }; - 55B218B13024B00100784721 /* FeatureIntroVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIntroVC.swift; sourceTree = ""; }; - 55B218B13024B00100784723 /* FeatureIntroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIntroView.swift; sourceTree = ""; }; 30D74AB12FEA1D5D0050EB2C /* ScheduleViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleViewModel.swift; sourceTree = ""; }; 30D74AB32FEA25B90050EB2C /* ViewedModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewedModel.swift; sourceTree = ""; }; 30D74AB52FEA34FF0050EB2C /* ItineraryAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItineraryAPI.swift; sourceTree = ""; }; @@ -634,13 +697,17 @@ 30EFF3E42FDAA93300EB35D4 /* PrivacyPolicyVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyPolicyVC.swift; sourceTree = ""; }; 30EFF3E62FDAA93D00EB35D4 /* PrivacyPolicyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyPolicyView.swift; sourceTree = ""; }; 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 = ""; }; 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; }; + 47922567FCD34DBE9186F8FD /* AppCatalogStore.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppCatalogStore.swift; path = AppRestrict/AppCatalogStore.swift; sourceTree = ""; }; + 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 = ""; }; 55B2179030217D6600784774 /* HomeView2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView2.swift; sourceTree = ""; }; 55B2179230218CE100784774 /* GroupMemberView2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberView2.swift; sourceTree = ""; }; 55B217943022DD6B00784774 /* 荆南波波黑-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "荆南波波黑-Bold.ttf"; sourceTree = ""; }; 55B217A03022F0A000784701 /* FontManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontManager.swift; sourceTree = ""; }; + 55B217A33022F0B100784707 /* UIDevice+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Extension.swift"; sourceTree = ""; }; 55B217B03022F0A000784710 /* GroupItineraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupItineraryView.swift; sourceTree = ""; }; 55B217C03022F0A000784720 /* MinePhotoWallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MinePhotoWallView.swift; sourceTree = ""; }; 55B217C23022F0A000784722 /* MemberPhoneReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberPhoneReportView.swift; sourceTree = ""; }; @@ -658,32 +725,62 @@ 55B218A13024A00100784707 /* CancelAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelAccountView.swift; sourceTree = ""; }; 55B218A13024A00100784709 /* MemberInfoVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberInfoVC.swift; sourceTree = ""; }; 55B218A13024A0010078470B /* MemberInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberInfoView.swift; sourceTree = ""; }; + 55B218B13024B00100784721 /* FeatureIntroVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIntroVC.swift; sourceTree = ""; }; + 55B218B13024B00100784723 /* FeatureIntroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIntroView.swift; sourceTree = ""; }; + 55B219C13024C00100784721 /* BubbleHeroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleHeroView.swift; sourceTree = ""; }; + 55B219C13024C00100784723 /* CreateBubbleSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleSetupView.swift; sourceTree = ""; }; + 55B219C13024C00100784725 /* BubbleKnowledgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleKnowledgeView.swift; sourceTree = ""; }; + 55B219C13024C00100784727 /* BubbleKnowledgeVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleKnowledgeVC.swift; sourceTree = ""; }; 55B21D6D3023117900784774 /* 优设标题黑_猫啃网.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "优设标题黑_猫啃网.ttf"; sourceTree = ""; }; 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissionPopView.swift; sourceTree = ""; }; 55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionEmojiCell.swift; sourceTree = ""; }; + 55C219C13024C00100784730 /* SearchLocationHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationHeaderView.swift; sourceTree = ""; }; + 5E5F5DC8694A001BF0C47147 /* PairGuideStepsView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PairGuideStepsView.swift; path = AppRestrict/PairGuideStepsView.swift; sourceTree = ""; }; + 6BAE595D39202A2958737BD3 /* SelectActivityVC.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SelectActivityVC.swift; path = AppRestrict/SelectActivityVC.swift; sourceTree = ""; }; + 787F87DD1CEBED1A4E1E9FC7 /* AppRestrictView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictView.swift; path = AppRestrict/AppRestrictView.swift; sourceTree = ""; }; + 7AEEA6CEA0CEAAB42C22752B /* AppRestrictManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictManager.swift; path = AppRestrict/AppRestrictManager.swift; sourceTree = ""; }; + 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 = ""; }; + 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 = ""; }; + AC46E80B848E4012D192E4CE /* LockDistractView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LockDistractView.swift; sourceTree = ""; }; + BE3D39F325FC7624C1D914D6 /* ITunesSearchService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ITunesSearchService.swift; path = AppRestrict/ITunesSearchService.swift; sourceTree = ""; }; + BFFBDE92B94E7004F6B642AD /* AppRestrictShieldSettingsVC.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictShieldSettingsVC.swift; path = AppRestrict/AppRestrictShieldSettingsVC.swift; sourceTree = ""; }; + D0112B7865829D5B6A4A0BD0 /* AppRestrictVC.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictVC.swift; path = AppRestrict/AppRestrictVC.swift; sourceTree = ""; }; + D64156B4A0E806901AB223FA /* DeviceActivityMonitorExtension.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DeviceActivityMonitorExtension.swift; path = DeviceActivityMonitorExtension/DeviceActivityMonitorExtension.swift; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; + EF9D223AB35744F92E0E3E77 /* FamilyActivityPickerHost.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FamilyActivityPickerHost.swift; path = AppRestrict/FamilyActivityPickerHost.swift; sourceTree = ""; }; + F9CFFB14BA2ECC259151BCDA /* LockDistractVC.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LockDistractVC.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ 30CCDF8E2FE3E63B00F5214A /* sound */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = sound; sourceTree = ""; }; 30CCDF902FE3E63B00F5214A /* video */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = video; sourceTree = ""; }; 30CCE01E2FE3E64700F5214A /* lotties */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = lotties; sourceTree = ""; }; 30D87CEF2FDFF52100E958FD /* TTGTagCollectionView */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = TTGTagCollectionView; sourceTree = ""; }; @@ -696,12 +793,63 @@ files = ( 30EFF3E82FCA8C7000227D26 /* AuthenticationServices.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; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 2234314E8DD57630628CC5C3 /* ShieldConfigurationExtension */ = { + isa = PBXGroup; + children = ( + 08002267FC4390B20D3510B3 /* ShieldConfigurationExtension.swift */, + ); + name = ShieldConfigurationExtension; + sourceTree = ""; + }; + 2509CBAFBD63A140B359BD7F /* DeviceActivityMonitorExtension */ = { + isa = PBXGroup; + children = ( + D64156B4A0E806901AB223FA /* DeviceActivityMonitorExtension.swift */, + ); + name = DeviceActivityMonitorExtension; + sourceTree = ""; + }; + 2F518AB6587CC227CC515D8C /* AppRestrict */ = { + isa = PBXGroup; + children = ( + 7AEEA6CEA0CEAAB42C22752B /* AppRestrictManager.swift */, + ); + name = AppRestrict; + sourceTree = ""; + }; 305A74C92FCA8C7000227D26 /* RxMoya */ = { isa = PBXGroup; children = ( @@ -981,6 +1129,7 @@ 305A752C2FCA8C7000227D26 /* URL */, 30D87CD52FDF9F1900E958FD /* MQTT */, 30C6666A2FFB7C3000E62B25 /* IAPManager */, + 2F518AB6587CC227CC515D8C /* AppRestrict */, ); path = Manager; sourceTree = ""; @@ -1156,6 +1305,8 @@ 30ACD54A2FF6332200174861 /* PopupWindow */, 30EFF3AD2FD7FF1400EB35D4 /* TextInput */, 30C6679D2FFB7FEF00E62B25 /* Share */, + F49062649CA8E5F9FA2F22A5 /* AppRestrict */, + B79BE06D49443FE82A811D5C /* LockDistract */, ); path = Section; sourceTree = ""; @@ -1564,15 +1715,6 @@ path = SOS; sourceTree = ""; }; - 55B218B13024B00100784720 /* Explore */ = { - isa = PBXGroup; - children = ( - 55B218B13024B00100784721 /* FeatureIntroVC.swift */, - 55B218B13024B00100784723 /* FeatureIntroView.swift */, - ); - path = Explore; - sourceTree = ""; - }; 30D74AAC2FEA13BD0050EB2C /* Schedule */ = { isa = PBXGroup; children = ( @@ -1728,6 +1870,9 @@ 3E4359092FC48D26003470A5 /* Products */, B07D45692FCECE07570D9B73 /* Pods */, 47CD8471BE2146A2656CF27E /* Frameworks */, + DA2257EC3F3167F4A6AFE759 /* AppRestrictShared */, + 2509CBAFBD63A140B359BD7F /* DeviceActivityMonitorExtension */, + 2234314E8DD57630628CC5C3 /* ShieldConfigurationExtension */, ); sourceTree = ""; }; @@ -1735,6 +1880,8 @@ isa = PBXGroup; children = ( 3E4359082FC48D26003470A5 /* QuickLocation.app */, + 98E91E51811235EAD0985421 /* DeviceActivityMonitorExtension.appex */, + 0C47E0012491DB1371FD5E53 /* ShieldConfigurationExtension.appex */, ); name = Products; sourceTree = ""; @@ -1744,6 +1891,12 @@ children = ( 30EFF3E92FCA8C7000227D26 /* AuthenticationServices.framework */, 475D33CAFA1E1911EB1F8D9F /* Pods_QuickLocation.framework */, + 4CD6B965A124276661A84C4C /* FamilyControls.framework */, + 2EE79029677394A46B143E75 /* ManagedSettings.framework */, + 88CC9D32980864EAD9F72004 /* DeviceActivity.framework */, + 7FE44DF1C46BDA7B98BC7859 /* ManagedSettingsUI.framework */, + 74B9CD2ABDBC4C8CE3107CDE /* iOS */, + 1E326B06736FE04DD7A5E96D /* UIKit.framework */, ); name = Frameworks; sourceTree = ""; @@ -1774,6 +1927,16 @@ path = TodayTrackDetail; sourceTree = ""; }; + 55B217DE302301000078473D /* PhoneReportDetail */ = { + isa = PBXGroup; + children = ( + 55B217D73023010000784736 /* PhoneReportDetailVC.swift */, + 55B217D93023010000784738 /* PhoneReportDetailView.swift */, + 55B217DB302301000078473A /* PhoneReportDetailViewModel.swift */, + ); + path = PhoneReportDetail; + sourceTree = ""; + }; 55B218A13024A00100784710 /* About */ = { isa = PBXGroup; children = ( @@ -1801,14 +1964,21 @@ path = MemberInfo; sourceTree = ""; }; - 55B217DE302301000078473D /* PhoneReportDetail */ = { + 55B218B13024B00100784720 /* Explore */ = { isa = PBXGroup; children = ( - 55B217D73023010000784736 /* PhoneReportDetailVC.swift */, - 55B217D93023010000784738 /* PhoneReportDetailView.swift */, - 55B217DB302301000078473A /* PhoneReportDetailViewModel.swift */, + 55B218B13024B00100784721 /* FeatureIntroVC.swift */, + 55B218B13024B00100784723 /* FeatureIntroView.swift */, ); - path = PhoneReportDetail; + path = Explore; + sourceTree = ""; + }; + 74B9CD2ABDBC4C8CE3107CDE /* iOS */ = { + isa = PBXGroup; + children = ( + 226CA98495C2884EBB1D1372 /* Foundation.framework */, + ); + name = iOS; sourceTree = ""; }; B07D45692FCECE07570D9B73 /* Pods */ = { @@ -1820,6 +1990,40 @@ path = Pods; sourceTree = ""; }; + B79BE06D49443FE82A811D5C /* LockDistract */ = { + isa = PBXGroup; + children = ( + F9CFFB14BA2ECC259151BCDA /* LockDistractVC.swift */, + AC46E80B848E4012D192E4CE /* LockDistractView.swift */, + ); + path = LockDistract; + sourceTree = ""; + }; + DA2257EC3F3167F4A6AFE759 /* AppRestrictShared */ = { + isa = PBXGroup; + children = ( + 1AB29D0618C4BE76F2CA5261 /* AppRestrictShared.swift */, + ); + name = AppRestrictShared; + sourceTree = ""; + }; + 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 = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -1833,10 +2037,13 @@ 3E4359062FC48D26003470A5 /* Resources */, 22E76BAEDC74B6177770F922 /* [CP] Embed Pods Frameworks */, 84E518A2C7E1AE699B07AC0D /* [CP] Copy Pods Resources */, + 63D74ED0B1CEAFA8DAC0A6C0 /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( + 0950A334CDF61EB8C1C94506 /* PBXTargetDependency */, + DCCA06707CFDC3C0D07BD8B0 /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( 30CCDF8E2FE3E63B00F5214A /* sound */, @@ -1849,6 +2056,40 @@ productReference = 3E4359082FC48D26003470A5 /* QuickLocation.app */; 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 */ /* Begin PBXProject section */ @@ -1879,6 +2120,8 @@ projectRoot = ""; targets = ( 3E4359072FC48D26003470A5 /* QuickLocation */, + B626CA1007794E780B5B1CB3 /* DeviceActivityMonitorExtension */, + E4257D157F2E24904E9B6630 /* ShieldConfigurationExtension */, ); }; /* End PBXProject section */ @@ -1896,6 +2139,21 @@ 55B217953022DD6B00784774 /* 荆南波波黑-Bold.ttf in Resources */, 55B21D6E3023117900784774 /* 优设标题黑_猫啃网.ttf 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; }; @@ -2283,11 +2541,57 @@ 305A771B2FCA8C7000227D26 /* ReusableView.swift in Sources */, 305A771C2FCA8C7000227D26 /* AppDelegate.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; }; /* 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 */ 305A76822FCA8C7000227D26 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; @@ -2310,6 +2614,61 @@ /* End PBXVariantGroup 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 */ = { isa = XCBuildConfiguration; baseConfigurationReference = DA16D49AA46D4F6838340B55 /* Pods-QuickLocation.debug.xcconfig */; @@ -2339,7 +2698,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; - IPHONEOS_DEPLOYMENT_TARGET = 15; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2393,7 +2752,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; - IPHONEOS_DEPLOYMENT_TARGET = 15; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2471,7 +2830,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.6; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -2529,7 +2888,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.6; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -2539,9 +2898,73 @@ }; 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 */ /* 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" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -2560,6 +2983,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + BAA60EF75453DD3B5A4775AF /* Build configuration list for PBXNativeTarget "ShieldConfigurationExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B66BC4F86D908B8200A8DC8E /* Release */, + A0AD067561B0A604B5DF5049 /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 3E4359002FC48D26003470A5 /* Project object */; diff --git a/QuickLocation/Assets.xcassets/AppRestrict/Contents.json b/QuickLocation/Assets.xcassets/AppRestrict/Contents.json new file mode 100644 index 00000000..15e0a992 --- /dev/null +++ b/QuickLocation/Assets.xcassets/AppRestrict/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { "author" : "xcode", "version" : 1 }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/QuickLocation/Assets.xcassets/AppRestrict/pair_action_delete.imageset/Contents.json b/QuickLocation/Assets.xcassets/AppRestrict/pair_action_delete.imageset/Contents.json new file mode 100644 index 00000000..c5d68312 --- /dev/null +++ b/QuickLocation/Assets.xcassets/AppRestrict/pair_action_delete.imageset/Contents.json @@ -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 } +} diff --git a/QuickLocation/Assets.xcassets/AppRestrict/pair_action_delete.imageset/pair_action_delete@2x.png b/QuickLocation/Assets.xcassets/AppRestrict/pair_action_delete.imageset/pair_action_delete@2x.png new file mode 100644 index 00000000..9da5d479 Binary files /dev/null and b/QuickLocation/Assets.xcassets/AppRestrict/pair_action_delete.imageset/pair_action_delete@2x.png differ diff --git a/QuickLocation/Assets.xcassets/AppRestrict/pair_action_delete.imageset/pair_action_delete@3x.png b/QuickLocation/Assets.xcassets/AppRestrict/pair_action_delete.imageset/pair_action_delete@3x.png new file mode 100644 index 00000000..3e952bd2 Binary files /dev/null and b/QuickLocation/Assets.xcassets/AppRestrict/pair_action_delete.imageset/pair_action_delete@3x.png differ diff --git a/QuickLocation/Assets.xcassets/AppRestrict/pair_action_link.imageset/Contents.json b/QuickLocation/Assets.xcassets/AppRestrict/pair_action_link.imageset/Contents.json new file mode 100644 index 00000000..5aaace1b --- /dev/null +++ b/QuickLocation/Assets.xcassets/AppRestrict/pair_action_link.imageset/Contents.json @@ -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 } +} diff --git a/QuickLocation/Assets.xcassets/AppRestrict/pair_action_link.imageset/pair_action_link@2x.png b/QuickLocation/Assets.xcassets/AppRestrict/pair_action_link.imageset/pair_action_link@2x.png new file mode 100644 index 00000000..5314143b Binary files /dev/null and b/QuickLocation/Assets.xcassets/AppRestrict/pair_action_link.imageset/pair_action_link@2x.png differ diff --git a/QuickLocation/Assets.xcassets/AppRestrict/pair_action_link.imageset/pair_action_link@3x.png b/QuickLocation/Assets.xcassets/AppRestrict/pair_action_link.imageset/pair_action_link@3x.png new file mode 100644 index 00000000..7d8e88df Binary files /dev/null and b/QuickLocation/Assets.xcassets/AppRestrict/pair_action_link.imageset/pair_action_link@3x.png differ diff --git a/QuickLocation/Assets.xcassets/AppRestrict/pair_badge_check.imageset/Contents.json b/QuickLocation/Assets.xcassets/AppRestrict/pair_badge_check.imageset/Contents.json new file mode 100644 index 00000000..be555ac0 --- /dev/null +++ b/QuickLocation/Assets.xcassets/AppRestrict/pair_badge_check.imageset/Contents.json @@ -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 } +} diff --git a/QuickLocation/Assets.xcassets/AppRestrict/pair_badge_check.imageset/pair_badge_check@2x.png b/QuickLocation/Assets.xcassets/AppRestrict/pair_badge_check.imageset/pair_badge_check@2x.png new file mode 100644 index 00000000..ed9f129c Binary files /dev/null and b/QuickLocation/Assets.xcassets/AppRestrict/pair_badge_check.imageset/pair_badge_check@2x.png differ diff --git a/QuickLocation/Assets.xcassets/AppRestrict/pair_badge_check.imageset/pair_badge_check@3x.png b/QuickLocation/Assets.xcassets/AppRestrict/pair_badge_check.imageset/pair_badge_check@3x.png new file mode 100644 index 00000000..dfbcf2ba Binary files /dev/null and b/QuickLocation/Assets.xcassets/AppRestrict/pair_badge_check.imageset/pair_badge_check@3x.png differ diff --git a/QuickLocation/Assets.xcassets/AppRestrict/pair_link_active.imageset/Contents.json b/QuickLocation/Assets.xcassets/AppRestrict/pair_link_active.imageset/Contents.json new file mode 100644 index 00000000..d177b2dd --- /dev/null +++ b/QuickLocation/Assets.xcassets/AppRestrict/pair_link_active.imageset/Contents.json @@ -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 } +} diff --git a/QuickLocation/Assets.xcassets/AppRestrict/pair_link_active.imageset/pair_link_active@2x.png b/QuickLocation/Assets.xcassets/AppRestrict/pair_link_active.imageset/pair_link_active@2x.png new file mode 100644 index 00000000..a4c78946 Binary files /dev/null and b/QuickLocation/Assets.xcassets/AppRestrict/pair_link_active.imageset/pair_link_active@2x.png differ diff --git a/QuickLocation/Assets.xcassets/AppRestrict/pair_link_active.imageset/pair_link_active@3x.png b/QuickLocation/Assets.xcassets/AppRestrict/pair_link_active.imageset/pair_link_active@3x.png new file mode 100644 index 00000000..4f49261c Binary files /dev/null and b/QuickLocation/Assets.xcassets/AppRestrict/pair_link_active.imageset/pair_link_active@3x.png differ diff --git a/QuickLocation/Assets.xcassets/AppRestrict/pair_link_inactive.imageset/Contents.json b/QuickLocation/Assets.xcassets/AppRestrict/pair_link_inactive.imageset/Contents.json new file mode 100644 index 00000000..2cb2e518 --- /dev/null +++ b/QuickLocation/Assets.xcassets/AppRestrict/pair_link_inactive.imageset/Contents.json @@ -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 } +} diff --git a/QuickLocation/Assets.xcassets/AppRestrict/pair_link_inactive.imageset/pair_link_inactive@2x.png b/QuickLocation/Assets.xcassets/AppRestrict/pair_link_inactive.imageset/pair_link_inactive@2x.png new file mode 100644 index 00000000..22e725ea Binary files /dev/null and b/QuickLocation/Assets.xcassets/AppRestrict/pair_link_inactive.imageset/pair_link_inactive@2x.png differ diff --git a/QuickLocation/Assets.xcassets/AppRestrict/pair_link_inactive.imageset/pair_link_inactive@3x.png b/QuickLocation/Assets.xcassets/AppRestrict/pair_link_inactive.imageset/pair_link_inactive@3x.png new file mode 100644 index 00000000..9a992fa9 Binary files /dev/null and b/QuickLocation/Assets.xcassets/AppRestrict/pair_link_inactive.imageset/pair_link_inactive@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Group/edit.imageset/Group_297@2x.png b/QuickLocation/Assets.xcassets/Group/edit.imageset/Group_297@2x.png index 8c6963d2..cfc9428a 100644 Binary files a/QuickLocation/Assets.xcassets/Group/edit.imageset/Group_297@2x.png and b/QuickLocation/Assets.xcassets/Group/edit.imageset/Group_297@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Group/edit.imageset/Group_297@3x.png b/QuickLocation/Assets.xcassets/Group/edit.imageset/Group_297@3x.png index f679b23c..c240862c 100644 Binary files a/QuickLocation/Assets.xcassets/Group/edit.imageset/Group_297@3x.png and b/QuickLocation/Assets.xcassets/Group/edit.imageset/Group_297@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Group/join_hero_bg.imageset/Contents.json b/QuickLocation/Assets.xcassets/Group/join_hero_bg.imageset/Contents.json new file mode 100644 index 00000000..5bdb198b --- /dev/null +++ b/QuickLocation/Assets.xcassets/Group/join_hero_bg.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/Group/join_hero_bg.imageset/join_hero_bg@2x.png b/QuickLocation/Assets.xcassets/Group/join_hero_bg.imageset/join_hero_bg@2x.png new file mode 100644 index 00000000..6af6e96c Binary files /dev/null and b/QuickLocation/Assets.xcassets/Group/join_hero_bg.imageset/join_hero_bg@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Group/join_hero_bg.imageset/join_hero_bg@3x.png b/QuickLocation/Assets.xcassets/Group/join_hero_bg.imageset/join_hero_bg@3x.png new file mode 100644 index 00000000..309a236a Binary files /dev/null and b/QuickLocation/Assets.xcassets/Group/join_hero_bg.imageset/join_hero_bg@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Group/join_scan.imageset/Contents.json b/QuickLocation/Assets.xcassets/Group/join_scan.imageset/Contents.json new file mode 100644 index 00000000..92a47906 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Group/join_scan.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/Group/join_scan.imageset/join_scan@2x.png b/QuickLocation/Assets.xcassets/Group/join_scan.imageset/join_scan@2x.png new file mode 100644 index 00000000..21a175b8 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Group/join_scan.imageset/join_scan@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Group/join_scan.imageset/join_scan@3x.png b/QuickLocation/Assets.xcassets/Group/join_scan.imageset/join_scan@3x.png new file mode 100644 index 00000000..0e3cce28 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Group/join_scan.imageset/join_scan@3x.png differ diff --git a/QuickLocation/Assets.xcassets/LockDistract/Contents.json b/QuickLocation/Assets.xcassets/LockDistract/Contents.json new file mode 100644 index 00000000..6e965652 --- /dev/null +++ b/QuickLocation/Assets.xcassets/LockDistract/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/QuickLocation/Assets.xcassets/LockDistract/app_unknown.imageset/Contents.json b/QuickLocation/Assets.xcassets/LockDistract/app_unknown.imageset/Contents.json new file mode 100644 index 00000000..3567e282 --- /dev/null +++ b/QuickLocation/Assets.xcassets/LockDistract/app_unknown.imageset/Contents.json @@ -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 } +} diff --git a/QuickLocation/Assets.xcassets/LockDistract/app_unknown.imageset/app_unknown@2x.png b/QuickLocation/Assets.xcassets/LockDistract/app_unknown.imageset/app_unknown@2x.png new file mode 100644 index 00000000..2c3766cf Binary files /dev/null and b/QuickLocation/Assets.xcassets/LockDistract/app_unknown.imageset/app_unknown@2x.png differ diff --git a/QuickLocation/Assets.xcassets/LockDistract/app_unknown.imageset/app_unknown@3x.png b/QuickLocation/Assets.xcassets/LockDistract/app_unknown.imageset/app_unknown@3x.png new file mode 100644 index 00000000..3cf1428f Binary files /dev/null and b/QuickLocation/Assets.xcassets/LockDistract/app_unknown.imageset/app_unknown@3x.png differ diff --git a/QuickLocation/Assets.xcassets/LockDistract/arrow_right.imageset/Contents.json b/QuickLocation/Assets.xcassets/LockDistract/arrow_right.imageset/Contents.json new file mode 100644 index 00000000..b62319bd --- /dev/null +++ b/QuickLocation/Assets.xcassets/LockDistract/arrow_right.imageset/Contents.json @@ -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 } +} diff --git a/QuickLocation/Assets.xcassets/LockDistract/arrow_right.imageset/arrow_right@2x.png b/QuickLocation/Assets.xcassets/LockDistract/arrow_right.imageset/arrow_right@2x.png new file mode 100644 index 00000000..204430a1 Binary files /dev/null and b/QuickLocation/Assets.xcassets/LockDistract/arrow_right.imageset/arrow_right@2x.png differ diff --git a/QuickLocation/Assets.xcassets/LockDistract/arrow_right.imageset/arrow_right@3x.png b/QuickLocation/Assets.xcassets/LockDistract/arrow_right.imageset/arrow_right@3x.png new file mode 100644 index 00000000..ad039524 Binary files /dev/null and b/QuickLocation/Assets.xcassets/LockDistract/arrow_right.imageset/arrow_right@3x.png differ diff --git a/QuickLocation/Assets.xcassets/LockDistract/section_star.imageset/Contents.json b/QuickLocation/Assets.xcassets/LockDistract/section_star.imageset/Contents.json new file mode 100644 index 00000000..bb170b0e --- /dev/null +++ b/QuickLocation/Assets.xcassets/LockDistract/section_star.imageset/Contents.json @@ -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 } +} diff --git a/QuickLocation/Assets.xcassets/LockDistract/section_star.imageset/section_star@2x.png b/QuickLocation/Assets.xcassets/LockDistract/section_star.imageset/section_star@2x.png new file mode 100644 index 00000000..53acb0cc Binary files /dev/null and b/QuickLocation/Assets.xcassets/LockDistract/section_star.imageset/section_star@2x.png differ diff --git a/QuickLocation/Assets.xcassets/LockDistract/section_star.imageset/section_star@3x.png b/QuickLocation/Assets.xcassets/LockDistract/section_star.imageset/section_star@3x.png new file mode 100644 index 00000000..b823565f Binary files /dev/null and b/QuickLocation/Assets.xcassets/LockDistract/section_star.imageset/section_star@3x.png differ diff --git a/QuickLocation/Assets.xcassets/LockDistract/wallpaper_1.imageset/Contents.json b/QuickLocation/Assets.xcassets/LockDistract/wallpaper_1.imageset/Contents.json new file mode 100644 index 00000000..e974552d --- /dev/null +++ b/QuickLocation/Assets.xcassets/LockDistract/wallpaper_1.imageset/Contents.json @@ -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 } +} diff --git a/QuickLocation/Assets.xcassets/LockDistract/wallpaper_1.imageset/wallpaper_1@2x.png b/QuickLocation/Assets.xcassets/LockDistract/wallpaper_1.imageset/wallpaper_1@2x.png new file mode 100644 index 00000000..9d6894d8 Binary files /dev/null and b/QuickLocation/Assets.xcassets/LockDistract/wallpaper_1.imageset/wallpaper_1@2x.png differ diff --git a/QuickLocation/Assets.xcassets/LockDistract/wallpaper_1.imageset/wallpaper_1@3x.png b/QuickLocation/Assets.xcassets/LockDistract/wallpaper_1.imageset/wallpaper_1@3x.png new file mode 100644 index 00000000..1afbfbd8 Binary files /dev/null and b/QuickLocation/Assets.xcassets/LockDistract/wallpaper_1.imageset/wallpaper_1@3x.png differ diff --git a/QuickLocation/Assets.xcassets/LockDistract/wallpaper_2.imageset/Contents.json b/QuickLocation/Assets.xcassets/LockDistract/wallpaper_2.imageset/Contents.json new file mode 100644 index 00000000..f796e3c5 --- /dev/null +++ b/QuickLocation/Assets.xcassets/LockDistract/wallpaper_2.imageset/Contents.json @@ -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 } +} diff --git a/QuickLocation/Assets.xcassets/LockDistract/wallpaper_2.imageset/wallpaper_2@2x.png b/QuickLocation/Assets.xcassets/LockDistract/wallpaper_2.imageset/wallpaper_2@2x.png new file mode 100644 index 00000000..fc9e8114 Binary files /dev/null and b/QuickLocation/Assets.xcassets/LockDistract/wallpaper_2.imageset/wallpaper_2@2x.png differ diff --git a/QuickLocation/Assets.xcassets/LockDistract/wallpaper_2.imageset/wallpaper_2@3x.png b/QuickLocation/Assets.xcassets/LockDistract/wallpaper_2.imageset/wallpaper_2@3x.png new file mode 100644 index 00000000..e61e34a2 Binary files /dev/null and b/QuickLocation/Assets.xcassets/LockDistract/wallpaper_2.imageset/wallpaper_2@3x.png differ diff --git a/QuickLocation/Assets.xcassets/LockDistract/wallpaper_3.imageset/Contents.json b/QuickLocation/Assets.xcassets/LockDistract/wallpaper_3.imageset/Contents.json new file mode 100644 index 00000000..cf7010c0 --- /dev/null +++ b/QuickLocation/Assets.xcassets/LockDistract/wallpaper_3.imageset/Contents.json @@ -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 } +} diff --git a/QuickLocation/Assets.xcassets/LockDistract/wallpaper_3.imageset/wallpaper_3@2x.png b/QuickLocation/Assets.xcassets/LockDistract/wallpaper_3.imageset/wallpaper_3@2x.png new file mode 100644 index 00000000..e6591678 Binary files /dev/null and b/QuickLocation/Assets.xcassets/LockDistract/wallpaper_3.imageset/wallpaper_3@2x.png differ diff --git a/QuickLocation/Assets.xcassets/LockDistract/wallpaper_3.imageset/wallpaper_3@3x.png b/QuickLocation/Assets.xcassets/LockDistract/wallpaper_3.imageset/wallpaper_3@3x.png new file mode 100644 index 00000000..2a66416d Binary files /dev/null and b/QuickLocation/Assets.xcassets/LockDistract/wallpaper_3.imageset/wallpaper_3@3x.png differ diff --git a/QuickLocation/Main/BaseViewController/BaseViewController.swift b/QuickLocation/Main/BaseViewController/BaseViewController.swift index 4ea4240c..07676362 100644 --- a/QuickLocation/Main/BaseViewController/BaseViewController.swift +++ b/QuickLocation/Main/BaseViewController/BaseViewController.swift @@ -44,7 +44,7 @@ class BaseViewController: UIViewController { // Do any additional setup after loading the view. fd_prefersNavigationBarHidden = isNavigationBarHidden - view.backgroundColor = .white//ThemeManager.shared.color.backgroundColor + //view.backgroundColor = .white//ThemeManager.shared.color.backgroundColor // setupNavigationBar() setupLeftItem() } diff --git a/QuickLocation/Manager/App/RouterManager.swift b/QuickLocation/Manager/App/RouterManager.swift index a2d6f3ea..cf3d07be 100644 --- a/QuickLocation/Manager/App/RouterManager.swift +++ b/QuickLocation/Manager/App/RouterManager.swift @@ -85,6 +85,10 @@ enum Route: String { case memberInfo = "memberInfo" /// 注销账号 case cancelAccount = "cancelAccount" + /// 限制 App 管理 + case appRestrict = "appRestrict" + /// 锁住分心(一键锁机) + case lockDistract = "lockDistract" /// 还在吗 / 打卡 case signIn = "signIn" /// SOS @@ -388,6 +392,18 @@ extension AppRouter: AppRouterProtocol { 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: - 还在吗 AppRouter.register(Route.signIn) { _, _ in SignInVC(lastLocation: nil) diff --git a/QuickLocation/Manager/AppRestrict/AppRestrictManager.swift b/QuickLocation/Manager/AppRestrict/AppRestrictManager.swift new file mode 100644 index 00000000..b546076c --- /dev/null +++ b/QuickLocation/Manager/AppRestrict/AppRestrictManager.swift @@ -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 { + 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)") + } + } +} diff --git a/QuickLocation/QuickLocation.entitlements b/QuickLocation/QuickLocation.entitlements index ca0d937d..4b2e32da 100644 --- a/QuickLocation/QuickLocation.entitlements +++ b/QuickLocation/QuickLocation.entitlements @@ -12,5 +12,11 @@ com.apple.developer.networking.wifi-info + com.apple.developer.family-controls + + com.apple.security.application-groups + + group.cn.zuomeng.jisuloca + diff --git a/QuickLocation/Section/AppRestrict/AppCatalogStore.swift b/QuickLocation/Section/AppRestrict/AppCatalogStore.swift new file mode 100644 index 00000000..f4542dc8 --- /dev/null +++ b/QuickLocation/Section/AppRestrict/AppCatalogStore.swift @@ -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 + ) + } + } +} diff --git a/QuickLocation/Section/AppRestrict/AppRestrictCell.swift b/QuickLocation/Section/AppRestrict/AppRestrictCell.swift new file mode 100644 index 00000000..3ca02021 --- /dev/null +++ b/QuickLocation/Section/AppRestrict/AppRestrictCell.swift @@ -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?() + } +} diff --git a/QuickLocation/Section/AppRestrict/AppRestrictShieldSettingsVC.swift b/QuickLocation/Section/AppRestrict/AppRestrictShieldSettingsVC.swift new file mode 100644 index 00000000..05a7f8f1 --- /dev/null +++ b/QuickLocation/Section/AppRestrict/AppRestrictShieldSettingsVC.swift @@ -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 } +} diff --git a/QuickLocation/Section/AppRestrict/AppRestrictVC.swift b/QuickLocation/Section/AppRestrict/AppRestrictVC.swift new file mode 100644 index 00000000..4aee623a --- /dev/null +++ b/QuickLocation/Section/AppRestrict/AppRestrictVC.swift @@ -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() + } +} diff --git a/QuickLocation/Section/AppRestrict/AppRestrictView.swift b/QuickLocation/Section/AppRestrict/AppRestrictView.swift new file mode 100644 index 00000000..291ed49d --- /dev/null +++ b/QuickLocation/Section/AppRestrict/AppRestrictView.swift @@ -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) + } +} diff --git a/QuickLocation/Section/AppRestrict/FamilyActivityPickerHost.swift b/QuickLocation/Section/AppRestrict/FamilyActivityPickerHost.swift new file mode 100644 index 00000000..4c847fbb --- /dev/null +++ b/QuickLocation/Section/AppRestrict/FamilyActivityPickerHost.swift @@ -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 + + 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 + + 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) + } +} diff --git a/QuickLocation/Section/AppRestrict/ITunesSearchService.swift b/QuickLocation/Section/AppRestrict/ITunesSearchService.swift new file mode 100644 index 00000000..08f3e086 --- /dev/null +++ b/QuickLocation/Section/AppRestrict/ITunesSearchService.swift @@ -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() + var merged: [AppCatalogItem] = [] + for item in local + remote { + guard !seen.contains(item.id) else { continue } + seen.insert(item.id) + merged.append(item) + } + return merged + } +} diff --git a/QuickLocation/Section/AppRestrict/PairGuideStepsView.swift b/QuickLocation/Section/AppRestrict/PairGuideStepsView.swift new file mode 100644 index 00000000..bdab70f7 --- /dev/null +++ b/QuickLocation/Section/AppRestrict/PairGuideStepsView.swift @@ -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.. 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? + + 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) + } +} diff --git a/QuickLocation/Section/AppRestrict/app_catalog.json b/QuickLocation/Section/AppRestrict/app_catalog.json new file mode 100644 index 00000000..9aa03f04 --- /dev/null +++ b/QuickLocation/Section/AppRestrict/app_catalog.json @@ -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": ["王者", "荣耀"] + } +] diff --git a/QuickLocation/Section/Explore/FeatureIntroVC.swift b/QuickLocation/Section/Explore/FeatureIntroVC.swift index e162e255..e891c8a3 100644 --- a/QuickLocation/Section/Explore/FeatureIntroVC.swift +++ b/QuickLocation/Section/Explore/FeatureIntroVC.swift @@ -33,6 +33,8 @@ final class FeatureIntroVC: BaseViewController { AppRouter.push(vc) case .createBubble: AppRouter.push(Route.createBubble) + case .lockDistract: + AppRouter.push(Route.lockDistract) case .searchLocation: AppRouter.push(Route.searchLocation) case .sos: diff --git a/QuickLocation/Section/Explore/FeatureIntroView.swift b/QuickLocation/Section/Explore/FeatureIntroView.swift index 8f1d75d5..ed885ae6 100644 --- a/QuickLocation/Section/Explore/FeatureIntroView.swift +++ b/QuickLocation/Section/Explore/FeatureIntroView.swift @@ -18,6 +18,7 @@ struct FeatureIntroItem { enum FeatureIntroAction { case signIn case createBubble + case lockDistract case searchLocation case sos case placeholder @@ -58,7 +59,7 @@ final class FeatureIntroView: UIView { titleColor: UIColor(hexStr: "#2F8A5B"), subtitleColor: UIColor(hexStr: "#6AAD88"), arrowTint: UIColor(hexStr: "#3EAE72"), - action: .placeholder + action: .lockDistract ), FeatureIntroItem( title: "飞鸽传书", diff --git a/QuickLocation/Section/Group/CreateGroup/CreateGroupVC.swift b/QuickLocation/Section/Group/CreateGroup/CreateGroupVC.swift index 915c3f15..175688eb 100644 --- a/QuickLocation/Section/Group/CreateGroup/CreateGroupVC.swift +++ b/QuickLocation/Section/Group/CreateGroup/CreateGroupVC.swift @@ -23,6 +23,7 @@ class CreateGroupVC: BaseViewController { override func viewDidLoad() { super.viewDidLoad() + view.backgroundColor = UIColor(hexStr: "#FAFAFA") bindViewModel() reactiveAction() @@ -31,18 +32,25 @@ class CreateGroupVC: BaseViewController { } private func reactiveAction() { - rootView.groupIconInputView.rx.tapGesture.subscribe { _ in - let vc = GroupIconListVC(iconIndex: "1") - 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.iconCarousel.onSelectIndex = { [weak self] index in + self?.viewModel.iconIndex = index + } - rootView.submitBtn.rx.tap.subscribe(onNext: { _ in - self.viewModel.requestCreateGroup() + rootView.submitBtn.rx.tap.subscribe(onNext: { [weak self] _ in + self?.viewModel.requestCreateGroup() }).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() { @@ -58,8 +66,29 @@ class CreateGroupVC: BaseViewController { .bind(to: rootView.tagView.rx.items(dataSource: dataSource)) .disposed(by: disposeBag) - rootView.tagView.rx.modelSelected(String.self) - .subscribe(viewModel.cellAction.inputs) + viewModel.output.sectionedItems + .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) } @@ -67,8 +96,22 @@ class CreateGroupVC: BaseViewController { private lazy var dataSource: RxCollectionViewSectionedReloadDataSource = { RxCollectionViewSectionedReloadDataSource { datasource, collectionView, indexPath, item in let cell: TagCell = collectionView.dequeueReusableCell(for: indexPath) - cell.configure(item, isSelected: self.viewModel.isSelected(tag: item)) + if item == CreateGroupViewModel.addTagToken { + cell.configureAsAdd() + } else { + cell.configure(item, isSelected: self.viewModel.isSelected(tag: item)) + } 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]) + } +} diff --git a/QuickLocation/Section/Group/CreateGroup/CreateGroupView.swift b/QuickLocation/Section/Group/CreateGroup/CreateGroupView.swift index 23134e87..131ec48b 100644 --- a/QuickLocation/Section/Group/CreateGroup/CreateGroupView.swift +++ b/QuickLocation/Section/Group/CreateGroup/CreateGroupView.swift @@ -14,6 +14,9 @@ class CreateGroupView: UIView { var disposeBag = DisposeBag() private let limitCount = 50 + private let tagMaxLength = 10 + + var onConfirmAddTag: ((String) -> Void)? private func setupRx() { groupNameTF.rx.text.orEmpty @@ -43,32 +46,57 @@ class CreateGroupView: UIView { } }) .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() { addSubview(navBgView) addSubview(navView) - + addSubview(scrollView) addSubview(submitBtn) - addSubview(titleLab) - addSubview(infoView) - infoView.addSubview(groupNameInputView) - groupNameInputView.addSubview(groupNameTitleLab) + + scrollView.addSubview(contentView) + contentView.addSubview(iconCarousel) + contentView.addSubview(typeNamePlaceholder) + contentView.addSubview(groupNameTitleLab) + contentView.addSubview(groupNameInputView) groupNameInputView.addSubview(groupNameTF) - - infoView.addSubview(groupIconInputView) - groupIconInputView.addSubview(groupIconTitleLab) - groupIconInputView.addSubview(groupIconImgView) - - infoView.addSubview(groupContentInputView) - groupContentInputView.addSubview(groupContentTitleLab) + contentView.addSubview(groupContentTitleLab) + contentView.addSubview(groupContentInputView) groupContentInputView.addSubview(groupContentTV) groupContentInputView.addSubview(placeholderLab) - - infoView.addSubview(tagInfoView) - tagInfoView.addSubview(tagTitleLab) - tagInfoView.addSubview(tagView) - tagInfoView.addSubview(tipsLab) + contentView.addSubview(tagTitleLab) + contentView.addSubview(tagView) navBgView.layoutChain .edges(excludingEdge: .bottom) @@ -78,91 +106,205 @@ class CreateGroupView: UIView { .edges(excludingEdge: .bottom) .height(kNaviHeight) - titleLab.layoutChain - .topToBottomOfView(navView, offset: 20) - .left(15) - submitBtn.layoutChain - .bottom(kSafeBottomMargin + 36) + .bottom(kSafeBottomMargin + 24) .centerX() .edgesHorzontal(30) - .height(50) + .height(56) - infoView.layoutChain - .topToBottomOfView(titleLab, offset: 20) - .edgesHorzontal(15) + scrollView.layoutChain + .topToBottomOfView(navView) + .edgesHorzontal() + .bottomToTopOfView(submitBtn, offset: -12) - groupNameInputView.layoutChain - .edges(excludingEdge: .bottom) + contentView.layoutChain + .edges() + .widthToView(scrollView) + + iconCarousel.layoutChain + .top(8) + .edgesHorzontal() + .height(130) + + typeNamePlaceholder.layoutChain + .topToBottomOfView(iconCarousel, offset: 4) + .centerX() groupNameTitleLab.layoutChain - .left(15) - .centerY() - .width(50) + .topToBottomOfView(typeNamePlaceholder, offset: 24) + .left(24) + + groupNameInputView.layoutChain + .topToBottomOfView(groupNameTitleLab, offset: 10) + .edgesHorzontal(20) + .height(50) groupNameTF.layoutChain .edgesVertical() - .leftToRightOfView(groupNameTitleLab, offset: 20) - .right(43) - - 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) + .left(16) + .right(48) groupContentTitleLab.layoutChain - .top(20) - .left(15) - .width(50) - + .topToBottomOfView(groupNameInputView, offset: 18) + .left(24) + + groupContentInputView.layoutChain + .topToBottomOfView(groupContentTitleLab, offset: 10) + .edgesHorzontal(20) + .height(96) + groupContentTV.layoutChain - .topToView(groupContentTitleLab, offset: -9) - .leftToRightOfView(groupContentTitleLab, offset: 18) - .right(52) - .bottom(10) - .height(20, relation: .greaterThanOrEqual) - + .top(8) + .left(12) + .right(48) + .bottom(8) + placeholderLab.layoutChain .topToView(groupContentTV, offset: 8) .leftToView(groupContentTV, offset: 5) - tagInfoView.layoutChain - .topToBottomOfView(groupContentInputView) - .leftToView(groupNameInputView) - .rightToView(groupNameInputView) - .bottom(20) - tagTitleLab.layoutChain - .top(20) - .left(15) - - tagView.layoutChain - .topToBottomOfView(tagTitleLab, offset: 10) - .edgesHorzontal(15) - .height(64) + .topToBottomOfView(groupContentInputView, offset: 18) + .left(24) - tipsLab.layoutChain - .topToBottomOfView(tagView, offset: 10) - .left(15) - .bottom() + tagView.layoutChain + .topToBottomOfView(tagTitleLab, offset: 12) + .edgesHorzontal(20) + .height(28) + .bottom(24) + + setupSheet() } + 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 = { let iv = UIImageView() iv.image = UIImage(named: "Common/navBar_bg_2") @@ -175,54 +317,73 @@ class CreateGroupView: UIView { return nav }() - lazy var titleLab: UILabel = { - let label = UILabel() - label.text = "编辑信息" - label.font = .systemFont(ofSize: 14, weight: .medium) - label.textColor = ThemeManager.shared.color.titleAuxColor - return label - }() - - lazy var infoView: UIView = { - let view = UIView() - view.backgroundColor = UIColor(hexStr: "#F5FBFF") - view.cornerRadius = 10 + lazy var scrollView: UIScrollView = { + let view = UIScrollView() + view.backgroundColor = .clear + view.keyboardDismissMode = .onDrag + view.showsVerticalScrollIndicator = false return view }() - // 圈子名称 - lazy var groupNameInputView: UIView = { + lazy var contentView: UIView = { let view = UIView() view.backgroundColor = .clear + return view + }() + + lazy var iconCarousel: CreateGroupIconCarouselView = { + let view = CreateGroupIconCarouselView() + return view + }() + + lazy var typeNamePlaceholder: UIView = { + let view = UIView() + view.backgroundColor = .white + view.cornerRadius = 10 + view.isUserInteractionEnabled = false - let icon = UIImageView() - icon.image = UIImage(named: "Group/edit") - view.addSubview(icon) - icon.layoutChain - .right(15) - .edgesVertical(20) - .width(20) - .height(20) - - let line = UIView() - line.backgroundColor = UIColor(hexStr: "#EEEEEE") - view.addSubview(line) - line.layoutChain - .edgesHorzontal(15) - .height(0.5) - .bottom() + view.addSubview(typeNameLab) + typeNameLab.layoutChain + .edgesVertical(10) + .edgesHorzontal(31) 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 = { let label = UILabel() label.text = "圈子名称" - label.font = .systemFont(ofSize: 12, weight: .medium) + label.font = .systemFont(ofSize: 14, weight: .medium) label.textColor = ThemeManager.shared.color.titleAuxColor 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 = { let textField = UITextField(frame: .zero) textField.font = UIFont.systemFont(ofSize: 14, weight: .medium) @@ -232,120 +393,64 @@ class CreateGroupView: UIView { 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 = { let label = UILabel() label.text = "圈子描述" - label.font = .systemFont(ofSize: 12, weight: .medium) + label.font = .systemFont(ofSize: 14, weight: .medium) label.textColor = ThemeManager.shared.color.titleAuxColor 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 = { let textView = UITextView() textView.backgroundColor = .clear textView.font = .systemFont(ofSize: 14, weight: .medium) 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 }() lazy var placeholderLab: UILabel = { let label = UILabel() - label.text = "请输入圈子描述" + label.text = "请输入.." label.textColor = ThemeManager.shared.color.contentColor label.font = .systemFont(ofSize: 14, weight: .medium) return label }() - // 标签 - lazy var tagInfoView: UIView = { - let view = UIView() - view.backgroundColor = .clear - return view - }() - lazy var tagTitleLab: UILabel = { let label = UILabel() label.text = "选择标签" - label.font = .systemFont(ofSize: 12, weight: .medium) + label.font = .systemFont(ofSize: 14, weight: .medium) label.textColor = ThemeManager.shared.color.titleAuxColor return label }() lazy var tagView: UICollectionView = { - let layout = UICollectionViewFlowLayout() - let cvWidth = kScreenWidth - 60 - let spacing: CGFloat = 6 - let itemW = (cvWidth - spacing * 3) / 4 - layout.itemSize = CGSize(width: itemW, height: 27) - layout.minimumInteritemSpacing = spacing - layout.minimumLineSpacing = 10 - + let layout = CreateGroupTagFlowLayout() + layout.minimumInteritemSpacing = 8 + layout.minimumLineSpacing = 8 + layout.sectionInset = .zero + let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) cv.backgroundColor = .clear cv.isScrollEnabled = false @@ -353,30 +458,74 @@ class CreateGroupView: UIView { 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 = { let btn = UIButton(type: .custom) btn.setTitle("创建", 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.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 }() override init(frame: CGRect) { super.init(frame: .zero) - backgroundColor = .white setupUI() setupRx() + backgroundColor = UIColor(hexStr: "#FAFAFA") } required init?(coder aDecoder: NSCoder) { @@ -384,50 +533,320 @@ 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.. 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) { + let proposedCenter = targetContentOffset.pointee.x + scrollView.bounds.width / 2 + var nearest = 0 + var minDist = CGFloat.greatestFiniteMagnitude + for i in 0.. [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 + final class TagCell: UICollectionViewCell { static let reuseId = "TagCell" - + private let label: UILabel = { let l = UILabel() l.font = .systemFont(ofSize: 12, weight: .medium) l.textAlignment = .center 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 - + override init(frame: CGRect) { super.init(frame: frame) contentView.addSubview(label) + contentView.addSubview(plusView) label.layoutChain.edges() - contentView.layer.cornerRadius = 4 - contentView.backgroundColor = UIColor(hexStr: "#E3F6FF") + plusView.layoutChain.edges() + contentView.layer.cornerRadius = 8 + contentView.backgroundColor = UIColor(hexStr: "#F0F2F5") updateStyle() } - + required init?(coder: NSCoder) { 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) { + plusView.isHidden = true + label.isHidden = false label.text = text - self.isTagSelected = isSelected + isTagSelected = isSelected + contentView.layer.cornerRadius = 8 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() { isTagSelected.toggle() updateStyle() } - + private func updateStyle() { if isTagSelected { label.textColor = UIColor(hexStr: "#16B3FF") + contentView.backgroundColor = UIColor(hexStr: "#E3F6FF") contentView.layer.borderWidth = 1 contentView.layer.borderColor = UIColor(hexStr: "#16B3FF").cgColor } else { label.textColor = ThemeManager.shared.color.titleAuxColor + contentView.backgroundColor = UIColor(hexStr: "#F0F2F5") contentView.layer.borderWidth = 0 } } diff --git a/QuickLocation/Section/Group/CreateGroup/CreateGroupViewModel.swift b/QuickLocation/Section/Group/CreateGroup/CreateGroupViewModel.swift index cc301f44..77f11623 100644 --- a/QuickLocation/Section/Group/CreateGroup/CreateGroupViewModel.swift +++ b/QuickLocation/Section/Group/CreateGroup/CreateGroupViewModel.swift @@ -13,6 +13,15 @@ import SwiftyUserDefaults typealias GroupTagListSectionModel = SectionModel class CreateGroupViewModel { + enum AddTagResult { + case success + case empty + case duplicate + } + + static let addTagToken = "__add_tag__" + static let tagMaxLength = 10 + struct Input { } @@ -26,7 +35,7 @@ class CreateGroupViewModel { private let sectionedItems = PublishSubject<[GroupTagListSectionModel]>() - private let tagList = ["私密", "游戏", "运动", "美食", + private var tagList = ["私密", "游戏", "运动", "美食", "自驾", "聚会", "旅行", "学习"] var selectedTagList: [String] = [] { @@ -56,9 +65,21 @@ class CreateGroupViewModel { 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: - 加载数据 func loadData() { - sectionedItems.onNext(tagList.mapSection()) + sectionedItems.onNext((tagList + [Self.addTagToken]).mapSection()) } // MARK: - Request diff --git a/QuickLocation/Section/Group/GroupView.swift b/QuickLocation/Section/Group/GroupView.swift index e4e847b6..7b022b78 100644 --- a/QuickLocation/Section/Group/GroupView.swift +++ b/QuickLocation/Section/Group/GroupView.swift @@ -642,7 +642,7 @@ final class CircleGroupCell: UITableViewCell { private let iconView: UIImageView = { let iv = UIImageView() iv.contentMode = .scaleAspectFill - iv.cornerRadius = 20 + iv.cornerRadius = 10 iv.clipsToBounds = true iv.backgroundColor = UIColor(hexStr: "#F0F0F0") return iv diff --git a/QuickLocation/Section/Group/Join/JoinGroupVC.swift b/QuickLocation/Section/Group/Join/JoinGroupVC.swift index fca50ab6..f7fb30ba 100644 --- a/QuickLocation/Section/Group/Join/JoinGroupVC.swift +++ b/QuickLocation/Section/Group/Join/JoinGroupVC.swift @@ -17,20 +17,24 @@ class JoinGroupVC: BaseViewController { rootView = JoinGroupView(frame: UIScreen.main.bounds) view = rootView } - + override func viewDidLoad() { super.viewDidLoad() + view.backgroundColor = .white - // Do any additional setup after loading the view. rootView.textField.rx.controlEvent(.editingDidEndOnExit).subscribe(onNext: { self.rootView.textField.resignFirstResponder() self.requestOperateGroup() }).disposed(by: disposeBag) - + rootView.submitBtn.rx.tap.subscribe(onNext: { _ in self.requestOperateGroup() }).disposed(by: disposeBag) - + + rootView.keyboardConfirmBtn.rx.tap.subscribe(onNext: { _ in + self.rootView.textField.resignFirstResponder() + }).disposed(by: disposeBag) + rootView.scanBtn.rx.tap.subscribe(onNext: { _ in let vc = ScanVC { code in self.rootView.textField.text = code @@ -39,12 +43,12 @@ class JoinGroupVC: BaseViewController { AppRouter.push(vc) }).disposed(by: disposeBag) } - + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) rootView.textField.becomeFirstResponder() } - + // MARK: - API private func requestOperateGroup() { guard rootView.inviteCode.count == 6 else { diff --git a/QuickLocation/Section/Group/Join/JoinGroupView.swift b/QuickLocation/Section/Group/Join/JoinGroupView.swift index d2be5760..e05daf28 100644 --- a/QuickLocation/Section/Group/Join/JoinGroupView.swift +++ b/QuickLocation/Section/Group/Join/JoinGroupView.swift @@ -12,263 +12,278 @@ import RxCocoa class JoinGroupView: UIView { var disposeBag = DisposeBag() - + var numberBtns: [UIButton] = [] - + var inviteCode: String = "" { - didSet { - for btn in numberBtns { - btn.setTitle("", for: .normal) - btn.isSelected = false - } - - let characters = inviteCode.map { String($0) } - for (index, character) in characters.enumerated() { - numberBtns[index].setTitle(character, for: .normal) - if index < numberBtns.count-1 { - numberBtns[index].isSelected = false - numberBtns[index+1].isSelected = true - } - } - } + didSet { updateCodeBoxes() } } - + @objc func numAction(button: UIButton) { textField.becomeFirstResponder() } - + + private func updateCodeBoxes() { + for btn in numberBtns { + btn.setTitle("", for: .normal) + btn.isSelected = false + } + + let characters = inviteCode.map { String($0) } + for (index, character) in characters.enumerated() { + numberBtns[index].setTitle(character, for: .normal) + numberBtns[index].isSelected = true + } + + let cursorIndex = characters.count + if cursorIndex < numberBtns.count { + numberBtns[cursorIndex].isSelected = true + } + } + private func setupRx() { textField.rx.text .orEmpty .subscribe(onNext: { text in if text.count < 7 { self.inviteCode = text - } - else { + } else { self.inviteCode = String(text.dropLast(2) + [text.last!]) self.textField.text = self.inviteCode } }) .disposed(by: disposeBag) - - } - + private func setupUI() { addSubview(navBgView) + addSubview(contentCard) 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(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 - .edges(excludingEdge: .bottom) - .heightToWidth(160/375) - + .top() + .edgesHorzontal() + .heightToWidth(253 / 375) + navView.layoutChain - .edges(excludingEdge: .bottom) + .top() + .edgesHorzontal() .height(kNaviHeight) - - titleLab.layoutChain - .topToBottomOfView(navView, offset: 21) + + contentCard.layoutChain + .topToBottomOfView(navBgView, offset: -60) + .edgesHorzontal() + .bottom() + + cardTitleLab.layoutChain + .top(17) .centerX() - + lineView.layoutChain - .topToBottomOfView(titleLab, offset: 58) + .topToBottomOfView(cardTitleLab, offset: 62) .centerX() - .width(10) - .height(4) - + .width(13) + .height(5) + number3.layoutChain .centerY(lineView) - .rightToLeftOfView(lineView, offset: -8) - .width(28) - .height(40) - + .rightToLeftOfView(lineView, offset: -10) + .width(36) + .height(52) + number2.layoutChain .topToView(number3) - .rightToLeftOfView(number3, offset: -8) + .rightToLeftOfView(number3, offset: -10) .widthToView(number3) .heightToView(number3) - + number1.layoutChain .topToView(number3) - .rightToLeftOfView(number2, offset: -8) + .rightToLeftOfView(number2, offset: -10) .widthToView(number3) .heightToView(number3) - + number4.layoutChain .centerY(lineView) - .leftToRightOfView(lineView, offset: 8) + .leftToRightOfView(lineView, offset: 10) .widthToView(number3) .heightToView(number3) - + number5.layoutChain .topToView(number3) - .leftToRightOfView(number4, offset: 8) + .leftToRightOfView(number4, offset: 10) .widthToView(number3) .heightToView(number3) - + number6.layoutChain .topToView(number3) - .leftToRightOfView(number5, offset: 8) + .leftToRightOfView(number5, offset: 10) .widthToView(number3) .heightToView(number3) - - tipsLab.layoutChain - .topToBottomOfView(lineView, offset: 38) - .centerX() - + submitBtn.layoutChain - .bottom(kSafeBottomMargin + 36) + .topToBottomOfView(number3, offset: 40) + .edgesHorzontal(24) + .height(56) + + tipsLab.layoutChain + .topToBottomOfView(submitBtn, offset: 16) .centerX() - .edgesHorzontal(30) - .height(50) + .edgesHorzontal(24) + + 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 = { let iv = UIImageView() - iv.image = UIImage(named: "Common/navBar_bg_2") + iv.image = UIImage(named: "Group/join_hero_bg") iv.contentMode = .scaleAspectFill + iv.clipsToBounds = true 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 = { - let nav = BaseNavigationView(title: "加入圈子") - nav.addRightButton(scanBtn) + let nav = BaseNavigationView(title: " ") return nav }() - + lazy var scanBtn: UIButton = { 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 }() - - lazy var titleLab: UILabel = { + + lazy var cardTitleLab: UILabel = { let label = UILabel() - label.text = "输入邀请码" - label.font = .systemFont(ofSize: 24, weight: .medium) - label.textColor = ThemeManager.shared.color.titleAuxColor + label.text = "请输入邀请码" + label.font = .systemFont(ofSize: 16, weight: .bold) + label.textColor = UIColor(hexStr: "#353B4F") label.textAlignment = .center return label }() - + lazy var tipsLab: UILabel = { let label = UILabel() - label.text = "向圈子创建者询问邀请码" - label.font = .systemFont(ofSize: 12, weight: .medium) - label.textColor = ThemeManager.shared.color.titleAuxColor + label.text = "输入好友分享的邀请码,添加好友专属圈子" + label.font = .systemFont(ofSize: 14, weight: .medium) + label.textColor = UIColor(hexStr: "#00ADFE") + label.textAlignment = .center + label.numberOfLines = 0 return label }() - + lazy var number1: 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.cornerRadius = 4 + let button = makeCodeButton() button.isSelected = true - button.addTarget(self, action: #selector(numAction), for: .touchUpInside) return button }() - - lazy var number2: 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 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 number2: UIButton = makeCodeButton() + lazy var number3: UIButton = makeCodeButton() + lazy var number4: UIButton = makeCodeButton() + lazy var number5: UIButton = makeCodeButton() + lazy var number6: UIButton = makeCodeButton() + lazy var lineView: UIView = { let view = UIView() - view.backgroundColor = UIColor(hexStr: "#16B3FF", alpha: 0.3) + view.backgroundColor = UIColor(hexStr: "#16B3FF") + view.cornerRadius = 0 return view }() - + lazy var submitBtn: UIButton = { let btn = UIButton(type: .custom) btn.setTitle("加入", for: .normal) - btn.setTitleColor(UIColor(hexStr: "#0F2846"), for: .normal) - btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) - btn.setBackgroundImage(UIImage(named: "Common/gradient_bg"), for: .normal) - btn.cornerRadius = 25 - + 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 }() - + + 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 = { let tf = UITextField() tf.isHidden = true tf.keyboardType = .asciiCapable tf.autocorrectionType = .no tf.returnKeyType = .done +// tf.inputAccessoryView = keyboardConfirmBar return tf }() - + override init(frame: CGRect) { super.init(frame: .zero) backgroundColor = .white setupUI() setupRx() - + numberBtns = [number1, number2, number3, number4, number5, number6] } diff --git a/QuickLocation/Section/Home/Bubble/CreateBubbleSetupView.swift b/QuickLocation/Section/Home/Bubble/CreateBubbleSetupView.swift index 6278fa35..133e9722 100644 --- a/QuickLocation/Section/Home/Bubble/CreateBubbleSetupView.swift +++ b/QuickLocation/Section/Home/Bubble/CreateBubbleSetupView.swift @@ -114,8 +114,8 @@ final class CreateBubbleSetupView: UIView { confirmBtn.setTitleColor(.white, for: .normal) confirmBtn.titleLabel?.font = FontManager.boboBold(18) confirmBtn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal) - confirmBtn.layer.cornerRadius = 16 - confirmBtn.layoutChain.left(20).right(20).bottom(34).height(56) + confirmBtn.cornerRadius = 20 + confirmBtn.layoutChain.left(30).right(30).bottom(34).height(56) } private func setupPicker() { diff --git a/QuickLocation/Section/LockDistract/LockDistractVC.swift b/QuickLocation/Section/LockDistract/LockDistractVC.swift new file mode 100644 index 00000000..c1f076c5 --- /dev/null +++ b/QuickLocation/Section/LockDistract/LockDistractVC.swift @@ -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) + } + } +} diff --git a/QuickLocation/Section/LockDistract/LockDistractView.swift b/QuickLocation/Section/LockDistract/LockDistractView.swift new file mode 100644 index 00000000..3228a6f3 --- /dev/null +++ b/QuickLocation/Section/LockDistract/LockDistractView.swift @@ -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" : "") + } +} diff --git a/QuickLocation/Section/Mine/PrivacyPolicy/PrivacyPolicyVC.swift b/QuickLocation/Section/Mine/PrivacyPolicy/PrivacyPolicyVC.swift index 45d074a7..3bf5f6c9 100644 --- a/QuickLocation/Section/Mine/PrivacyPolicy/PrivacyPolicyVC.swift +++ b/QuickLocation/Section/Mine/PrivacyPolicy/PrivacyPolicyVC.swift @@ -11,8 +11,13 @@ import RxCocoa import RxDataSources struct PrivacyPolicyItem { + enum Destination { + case web(String) + case appRestrict + } + let name: String - let url: String + let destination: Destination } class PrivacyPolicyVC: BaseViewController { @@ -25,9 +30,10 @@ class PrivacyPolicyVC: BaseViewController { } private let list: [PrivacyPolicyItem] = [ - PrivacyPolicyItem(name: "用户协议", url: URLManager.shared.userAgreementUrl), - PrivacyPolicyItem(name: "隐私政策", url: URLManager.shared.privacyPolicyUrl), - PrivacyPolicyItem(name: "儿童隐私政策", url: URLManager.shared.kidsPrivacyUrl) + PrivacyPolicyItem(name: "用户协议", destination: .web(URLManager.shared.userAgreementUrl)), + PrivacyPolicyItem(name: "隐私政策", destination: .web(URLManager.shared.privacyPolicyUrl)), + PrivacyPolicyItem(name: "儿童隐私政策", destination: .web(URLManager.shared.kidsPrivacyUrl)), + PrivacyPolicyItem(name: "应用配对库", destination: .appRestrict) ] override func viewDidLoad() { @@ -50,7 +56,12 @@ class PrivacyPolicyVC: BaseViewController { // 点击跳转 rootView.tableView.rx.modelSelected(PrivacyPolicyItem.self) .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) } diff --git a/ShieldConfigurationExtension/Info.plist b/ShieldConfigurationExtension/Info.plist new file mode 100644 index 00000000..cb5feed2 --- /dev/null +++ b/ShieldConfigurationExtension/Info.plist @@ -0,0 +1,27 @@ + + + + + CFBundleDisplayName + ShieldConfiguration + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSExtension + + NSExtensionPointIdentifier + com.apple.ManagedSettingsUI.shield-configuration-service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).ShieldConfigurationExtension + + + diff --git a/ShieldConfigurationExtension/ShieldConfigurationExtension.entitlements b/ShieldConfigurationExtension/ShieldConfigurationExtension.entitlements new file mode 100644 index 00000000..4a8cc927 --- /dev/null +++ b/ShieldConfigurationExtension/ShieldConfigurationExtension.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.developer.family-controls + + com.apple.security.application-groups + + group.cn.zuomeng.jisuloca + + + diff --git a/ShieldConfigurationExtension/ShieldConfigurationExtension.swift b/ShieldConfigurationExtension/ShieldConfigurationExtension.swift new file mode 100644 index 00000000..5adea059 --- /dev/null +++ b/ShieldConfigurationExtension/ShieldConfigurationExtension.swift @@ -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) + ) + } +} diff --git a/scripts/add_app_restrict_targets.rb b/scripts/add_app_restrict_targets.rb new file mode 100644 index 00000000..00f32137 --- /dev/null +++ b/scripts/add_app_restrict_targets.rb @@ -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' diff --git a/scripts/add_lock_distract_files.rb b/scripts/add_lock_distract_files.rb new file mode 100644 index 00000000..c2eea6a0 --- /dev/null +++ b/scripts/add_lock_distract_files.rb @@ -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'