|
|
@ -47,4 +47,5 @@ npm-debug.log
|
|||
screenshot/
|
||||
.opencode/
|
||||
.cursor/
|
||||
.codex/
|
||||
openspec/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,264 @@
|
|||
//
|
||||
// AppRestrictShared.swift
|
||||
// Shared between main app and Screen Time extensions.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import FamilyControls
|
||||
import ManagedSettings
|
||||
import UIKit
|
||||
|
||||
enum AppRestrictShared {
|
||||
static let appGroupId = "group.cn.zuomeng.jisuloca"
|
||||
static let storeName = ManagedSettingsStore.Name("appRestrict")
|
||||
static let activityName = "appRestrict.monitoring"
|
||||
|
||||
static let selectionKey = "appRestrict.selection"
|
||||
static let enabledTokensKey = "appRestrict.enabledTokens"
|
||||
static let linksKey = "appRestrict.links"
|
||||
static let shieldConfigKey = "appRestrict.shieldConfig"
|
||||
static let customImageFileName = "shield_custom.jpg"
|
||||
|
||||
static var defaults: UserDefaults {
|
||||
UserDefaults(suiteName: appGroupId) ?? .standard
|
||||
}
|
||||
|
||||
static var containerURL: URL? {
|
||||
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupId)
|
||||
}
|
||||
|
||||
static var customImageURL: URL? {
|
||||
containerURL?.appendingPathComponent(customImageFileName)
|
||||
}
|
||||
|
||||
static func makeStore() -> ManagedSettingsStore {
|
||||
ManagedSettingsStore(named: storeName)
|
||||
}
|
||||
}
|
||||
|
||||
struct AppRestrictLinkRecord: Codable, Equatable {
|
||||
let catalogId: String
|
||||
let tokenData: Data
|
||||
let displayName: String?
|
||||
let iconURL: String?
|
||||
|
||||
init(catalogId: String, tokenData: Data, displayName: String? = nil, iconURL: String? = nil) {
|
||||
self.catalogId = catalogId
|
||||
self.tokenData = tokenData
|
||||
self.displayName = displayName
|
||||
self.iconURL = iconURL
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
catalogId = try container.decode(String.self, forKey: .catalogId)
|
||||
tokenData = try container.decode(Data.self, forKey: .tokenData)
|
||||
displayName = try container.decodeIfPresent(String.self, forKey: .displayName)
|
||||
iconURL = try container.decodeIfPresent(String.self, forKey: .iconURL)
|
||||
}
|
||||
}
|
||||
|
||||
struct AppRestrictShieldConfig: Codable, Equatable {
|
||||
enum ImageSource: String, Codable {
|
||||
case presetDefault
|
||||
case presetFocus
|
||||
case presetCalm
|
||||
case album
|
||||
}
|
||||
|
||||
var title: String
|
||||
var subtitle: String
|
||||
var primaryButtonLabel: String
|
||||
var imageSource: ImageSource
|
||||
|
||||
static let `default` = AppRestrictShieldConfig(
|
||||
title: "该应用已被锁定",
|
||||
subtitle: "专注当下,稍后再回来吧",
|
||||
primaryButtonLabel: "知道了",
|
||||
imageSource: .presetDefault
|
||||
)
|
||||
}
|
||||
|
||||
enum AppRestrictTokenCodec {
|
||||
static func encode(_ token: ApplicationToken) -> Data? {
|
||||
try? PropertyListEncoder().encode(token)
|
||||
}
|
||||
|
||||
static func decode(_ data: Data) -> ApplicationToken? {
|
||||
try? PropertyListDecoder().decode(ApplicationToken.self, from: data)
|
||||
}
|
||||
|
||||
static func encodeSelection(_ selection: FamilyActivitySelection) -> Data? {
|
||||
try? PropertyListEncoder().encode(selection)
|
||||
}
|
||||
|
||||
static func decodeSelection(_ data: Data) -> FamilyActivitySelection? {
|
||||
try? PropertyListDecoder().decode(FamilyActivitySelection.self, from: data)
|
||||
}
|
||||
|
||||
static func encodeTokenSet(_ tokens: Set<ApplicationToken>) -> Data? {
|
||||
let datas = tokens.compactMap { encode($0) }
|
||||
return try? PropertyListEncoder().encode(datas)
|
||||
}
|
||||
|
||||
static func decodeTokenSet(_ data: Data) -> Set<ApplicationToken> {
|
||||
guard let datas = try? PropertyListDecoder().decode([Data].self, from: data) else {
|
||||
return []
|
||||
}
|
||||
return Set(datas.compactMap { decode($0) })
|
||||
}
|
||||
}
|
||||
|
||||
enum AppRestrictSharedStore {
|
||||
static var selection: FamilyActivitySelection {
|
||||
get {
|
||||
guard let data = AppRestrictShared.defaults.data(forKey: AppRestrictShared.selectionKey),
|
||||
let value = AppRestrictTokenCodec.decodeSelection(data) else {
|
||||
return FamilyActivitySelection()
|
||||
}
|
||||
return value
|
||||
}
|
||||
set {
|
||||
if let data = AppRestrictTokenCodec.encodeSelection(newValue) {
|
||||
AppRestrictShared.defaults.set(data, forKey: AppRestrictShared.selectionKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static var enabledTokens: Set<ApplicationToken> {
|
||||
get {
|
||||
guard let data = AppRestrictShared.defaults.data(forKey: AppRestrictShared.enabledTokensKey) else {
|
||||
return []
|
||||
}
|
||||
return AppRestrictTokenCodec.decodeTokenSet(data)
|
||||
}
|
||||
set {
|
||||
if let data = AppRestrictTokenCodec.encodeTokenSet(newValue) {
|
||||
AppRestrictShared.defaults.set(data, forKey: AppRestrictShared.enabledTokensKey)
|
||||
} else {
|
||||
AppRestrictShared.defaults.removeObject(forKey: AppRestrictShared.enabledTokensKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static var links: [AppRestrictLinkRecord] {
|
||||
get {
|
||||
guard let data = AppRestrictShared.defaults.data(forKey: AppRestrictShared.linksKey),
|
||||
let value = try? JSONDecoder().decode([AppRestrictLinkRecord].self, from: data) else {
|
||||
return []
|
||||
}
|
||||
return value
|
||||
}
|
||||
set {
|
||||
if let data = try? JSONEncoder().encode(newValue) {
|
||||
AppRestrictShared.defaults.set(data, forKey: AppRestrictShared.linksKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static var shieldConfig: AppRestrictShieldConfig {
|
||||
get {
|
||||
guard let data = AppRestrictShared.defaults.data(forKey: AppRestrictShared.shieldConfigKey),
|
||||
let value = try? JSONDecoder().decode(AppRestrictShieldConfig.self, from: data) else {
|
||||
return .default
|
||||
}
|
||||
return value
|
||||
}
|
||||
set {
|
||||
if let data = try? JSONEncoder().encode(newValue) {
|
||||
AppRestrictShared.defaults.set(data, forKey: AppRestrictShared.shieldConfigKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func linkRecord(for token: ApplicationToken) -> AppRestrictLinkRecord? {
|
||||
guard let data = AppRestrictTokenCodec.encode(token) else { return nil }
|
||||
return links.first(where: { $0.tokenData == data })
|
||||
}
|
||||
|
||||
static func catalogId(for token: ApplicationToken) -> String? {
|
||||
linkRecord(for: token)?.catalogId
|
||||
}
|
||||
|
||||
static func setLink(
|
||||
catalogId: String,
|
||||
token: ApplicationToken,
|
||||
displayName: String? = nil,
|
||||
iconURL: String? = nil
|
||||
) {
|
||||
guard let data = AppRestrictTokenCodec.encode(token) else { return }
|
||||
var list = links.filter { $0.tokenData != data && $0.catalogId != catalogId }
|
||||
list.append(AppRestrictLinkRecord(
|
||||
catalogId: catalogId,
|
||||
tokenData: data,
|
||||
displayName: displayName,
|
||||
iconURL: iconURL
|
||||
))
|
||||
links = list
|
||||
}
|
||||
|
||||
static func removeLink(for token: ApplicationToken) {
|
||||
guard let data = AppRestrictTokenCodec.encode(token) else { return }
|
||||
links = links.filter { $0.tokenData != data }
|
||||
}
|
||||
|
||||
static func applyShield(for tokens: Set<ApplicationToken>) {
|
||||
let store = AppRestrictShared.makeStore()
|
||||
if tokens.isEmpty {
|
||||
store.clearAllSettings()
|
||||
} else {
|
||||
store.shield.applications = tokens
|
||||
}
|
||||
}
|
||||
|
||||
static func clearShield() {
|
||||
AppRestrictShared.makeStore().clearAllSettings()
|
||||
}
|
||||
|
||||
static func loadShieldImage() -> UIImage? {
|
||||
let config = shieldConfig
|
||||
switch config.imageSource {
|
||||
case .album:
|
||||
if let url = AppRestrictShared.customImageURL,
|
||||
let data = try? Data(contentsOf: url),
|
||||
let image = UIImage(data: data) {
|
||||
return image
|
||||
}
|
||||
fallthrough
|
||||
case .presetDefault:
|
||||
return UIImage(named: "AppRestrict/shield_preset_default")
|
||||
?? UIImage(systemName: "lock.shield.fill")
|
||||
case .presetFocus:
|
||||
return UIImage(named: "AppRestrict/shield_preset_focus")
|
||||
?? UIImage(systemName: "brain.head.profile")
|
||||
case .presetCalm:
|
||||
return UIImage(named: "AppRestrict/shield_preset_calm")
|
||||
?? UIImage(systemName: "leaf.fill")
|
||||
}
|
||||
}
|
||||
|
||||
static func saveCustomImage(_ image: UIImage) -> Bool {
|
||||
guard let url = AppRestrictShared.customImageURL else { return false }
|
||||
let resized = image.appRestrictResized(maxSide: 720)
|
||||
guard let data = resized.jpegData(compressionQuality: 0.82) else { return false }
|
||||
do {
|
||||
try data.write(to: url, options: .atomic)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension UIImage {
|
||||
func appRestrictResized(maxSide: CGFloat) -> UIImage {
|
||||
let maxCurrent = max(size.width, size.height)
|
||||
guard maxCurrent > maxSide, maxCurrent > 0 else { return self }
|
||||
let scale = maxSide / maxCurrent
|
||||
let newSize = CGSize(width: size.width * scale, height: size.height * scale)
|
||||
let renderer = UIGraphicsImageRenderer(size: newSize)
|
||||
return renderer.image { _ in
|
||||
draw(in: CGRect(origin: .zero, size: newSize))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.family-controls</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.cn.zuomeng.jisuloca</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>DeviceActivityMonitor</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.deviceactivity.monitor-extension</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).DeviceActivityMonitorExtension</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
2
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
|
||||
|
|
|
|||
|
|
@ -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 = "<group>"; };
|
||||
0C47E0012491DB1371FD5E53 /* ShieldConfigurationExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShieldConfigurationExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
1AB29D0618C4BE76F2CA5261 /* AppRestrictShared.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictShared.swift; path = AppRestrictShared/AppRestrictShared.swift; sourceTree = "<group>"; };
|
||||
1E326B06736FE04DD7A5E96D /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
|
||||
226CA98495C2884EBB1D1372 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; };
|
||||
2EE79029677394A46B143E75 /* ManagedSettings.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ManagedSettings.framework; path = System/Library/Frameworks/ManagedSettings.framework; sourceTree = SDKROOT; };
|
||||
305A74C62FCA8C7000227D26 /* MoyaProvider+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MoyaProvider+Rx.swift"; sourceTree = "<group>"; };
|
||||
305A74C72FCA8C7000227D26 /* Observable+Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Observable+Response.swift"; sourceTree = "<group>"; };
|
||||
305A74C82FCA8C7000227D26 /* Single+Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Single+Response.swift"; sourceTree = "<group>"; };
|
||||
|
|
@ -372,7 +443,6 @@
|
|||
305A74F62FCA8C7000227D26 /* UIApplicationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationExtension.swift; sourceTree = "<group>"; };
|
||||
305A74F72FCA8C7000227D26 /* UIButton+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+Extension.swift"; sourceTree = "<group>"; };
|
||||
305A74F82FCA8C7000227D26 /* UIColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extension.swift"; sourceTree = "<group>"; };
|
||||
55B217A33022F0B100784707 /* UIDevice+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Extension.swift"; sourceTree = "<group>"; };
|
||||
305A74F92FCA8C7000227D26 /* UIFont+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Extension.swift"; sourceTree = "<group>"; };
|
||||
305A74FA2FCA8C7000227D26 /* UIImage+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Extension.swift"; sourceTree = "<group>"; };
|
||||
305A74FB2FCA8C7000227D26 /* UIImage+Resource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Resource.swift"; sourceTree = "<group>"; };
|
||||
|
|
@ -518,11 +588,6 @@
|
|||
30A87A632FEE75520095E7C6 /* CreateBubbleTipsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleTipsView.swift; sourceTree = "<group>"; };
|
||||
30A87A652FEE843E0095E7C6 /* CreateBubbleDoneView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleDoneView.swift; sourceTree = "<group>"; };
|
||||
30A87A672FEE86560095E7C6 /* CreateBubblePopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubblePopView.swift; sourceTree = "<group>"; };
|
||||
55B219C13024C00100784721 /* BubbleHeroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleHeroView.swift; sourceTree = "<group>"; };
|
||||
55B219C13024C00100784723 /* CreateBubbleSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleSetupView.swift; sourceTree = "<group>"; };
|
||||
55B219C13024C00100784725 /* BubbleKnowledgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleKnowledgeView.swift; sourceTree = "<group>"; };
|
||||
55B219C13024C00100784727 /* BubbleKnowledgeVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleKnowledgeVC.swift; sourceTree = "<group>"; };
|
||||
55C219C13024C00100784730 /* SearchLocationHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationHeaderView.swift; sourceTree = "<group>"; };
|
||||
30A87A6A2FEF5B950095E7C6 /* SearchLocationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationView.swift; sourceTree = "<group>"; };
|
||||
30A87A6C2FEF5BA10095E7C6 /* SearchLocationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationVC.swift; sourceTree = "<group>"; };
|
||||
30A87A6E2FEF7BE40095E7C6 /* SearchLocationResultVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationResultVC.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -571,8 +636,6 @@
|
|||
30D74AAA2FE8C7700050EB2C /* GPSSignalHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GPSSignalHelper.swift; sourceTree = "<group>"; };
|
||||
30D74AAD2FEA13E00050EB2C /* ScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleView.swift; sourceTree = "<group>"; };
|
||||
30D74AAF2FEA13ED0050EB2C /* ScheduleVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleVC.swift; sourceTree = "<group>"; };
|
||||
55B218B13024B00100784721 /* FeatureIntroVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIntroVC.swift; sourceTree = "<group>"; };
|
||||
55B218B13024B00100784723 /* FeatureIntroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIntroView.swift; sourceTree = "<group>"; };
|
||||
30D74AB12FEA1D5D0050EB2C /* ScheduleViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleViewModel.swift; sourceTree = "<group>"; };
|
||||
30D74AB32FEA25B90050EB2C /* ViewedModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewedModel.swift; sourceTree = "<group>"; };
|
||||
30D74AB52FEA34FF0050EB2C /* ItineraryAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItineraryAPI.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -634,13 +697,17 @@
|
|||
30EFF3E42FDAA93300EB35D4 /* PrivacyPolicyVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyPolicyVC.swift; sourceTree = "<group>"; };
|
||||
30EFF3E62FDAA93D00EB35D4 /* PrivacyPolicyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyPolicyView.swift; sourceTree = "<group>"; };
|
||||
30EFF3E92FCA8C7000227D26 /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = System/Library/Frameworks/AuthenticationServices.framework; sourceTree = SDKROOT; };
|
||||
3C79865C3ED165AFDD091C29 /* app_catalog.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = app_catalog.json; path = AppRestrict/app_catalog.json; sourceTree = "<group>"; };
|
||||
3E4359082FC48D26003470A5 /* QuickLocation.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QuickLocation.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
475D33CAFA1E1911EB1F8D9F /* Pods_QuickLocation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_QuickLocation.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
47922567FCD34DBE9186F8FD /* AppCatalogStore.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppCatalogStore.swift; path = AppRestrict/AppCatalogStore.swift; sourceTree = "<group>"; };
|
||||
4CD6B965A124276661A84C4C /* FamilyControls.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FamilyControls.framework; path = System/Library/Frameworks/FamilyControls.framework; sourceTree = SDKROOT; };
|
||||
557E8D5230187A5C0032AB51 /* BaseNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseNavigationView.swift; sourceTree = "<group>"; };
|
||||
55B2179030217D6600784774 /* HomeView2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView2.swift; sourceTree = "<group>"; };
|
||||
55B2179230218CE100784774 /* GroupMemberView2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberView2.swift; sourceTree = "<group>"; };
|
||||
55B217943022DD6B00784774 /* 荆南波波黑-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "荆南波波黑-Bold.ttf"; sourceTree = "<group>"; };
|
||||
55B217A03022F0A000784701 /* FontManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontManager.swift; sourceTree = "<group>"; };
|
||||
55B217A33022F0B100784707 /* UIDevice+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Extension.swift"; sourceTree = "<group>"; };
|
||||
55B217B03022F0A000784710 /* GroupItineraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupItineraryView.swift; sourceTree = "<group>"; };
|
||||
55B217C03022F0A000784720 /* MinePhotoWallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MinePhotoWallView.swift; sourceTree = "<group>"; };
|
||||
55B217C23022F0A000784722 /* MemberPhoneReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberPhoneReportView.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -658,32 +725,62 @@
|
|||
55B218A13024A00100784707 /* CancelAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelAccountView.swift; sourceTree = "<group>"; };
|
||||
55B218A13024A00100784709 /* MemberInfoVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberInfoVC.swift; sourceTree = "<group>"; };
|
||||
55B218A13024A0010078470B /* MemberInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberInfoView.swift; sourceTree = "<group>"; };
|
||||
55B218B13024B00100784721 /* FeatureIntroVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIntroVC.swift; sourceTree = "<group>"; };
|
||||
55B218B13024B00100784723 /* FeatureIntroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIntroView.swift; sourceTree = "<group>"; };
|
||||
55B219C13024C00100784721 /* BubbleHeroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleHeroView.swift; sourceTree = "<group>"; };
|
||||
55B219C13024C00100784723 /* CreateBubbleSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleSetupView.swift; sourceTree = "<group>"; };
|
||||
55B219C13024C00100784725 /* BubbleKnowledgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleKnowledgeView.swift; sourceTree = "<group>"; };
|
||||
55B219C13024C00100784727 /* BubbleKnowledgeVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleKnowledgeVC.swift; sourceTree = "<group>"; };
|
||||
55B21D6D3023117900784774 /* 优设标题黑_猫啃网.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "优设标题黑_猫啃网.ttf"; sourceTree = "<group>"; };
|
||||
55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissionPopView.swift; sourceTree = "<group>"; };
|
||||
55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionEmojiCell.swift; sourceTree = "<group>"; };
|
||||
55C219C13024C00100784730 /* SearchLocationHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationHeaderView.swift; sourceTree = "<group>"; };
|
||||
5E5F5DC8694A001BF0C47147 /* PairGuideStepsView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PairGuideStepsView.swift; path = AppRestrict/PairGuideStepsView.swift; sourceTree = "<group>"; };
|
||||
6BAE595D39202A2958737BD3 /* SelectActivityVC.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SelectActivityVC.swift; path = AppRestrict/SelectActivityVC.swift; sourceTree = "<group>"; };
|
||||
787F87DD1CEBED1A4E1E9FC7 /* AppRestrictView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictView.swift; path = AppRestrict/AppRestrictView.swift; sourceTree = "<group>"; };
|
||||
7AEEA6CEA0CEAAB42C22752B /* AppRestrictManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictManager.swift; path = AppRestrict/AppRestrictManager.swift; sourceTree = "<group>"; };
|
||||
7FE44DF1C46BDA7B98BC7859 /* ManagedSettingsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ManagedSettingsUI.framework; path = System/Library/Frameworks/ManagedSettingsUI.framework; sourceTree = SDKROOT; };
|
||||
88CC9D32980864EAD9F72004 /* DeviceActivity.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DeviceActivity.framework; path = System/Library/Frameworks/DeviceActivity.framework; sourceTree = SDKROOT; };
|
||||
89519661B70A0ADEC2A76032 /* AppRestrictCell.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictCell.swift; path = AppRestrict/AppRestrictCell.swift; sourceTree = "<group>"; };
|
||||
98E91E51811235EAD0985421 /* DeviceActivityMonitorExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = DeviceActivityMonitorExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
A1B2C3D52FDABC8C009215C1 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
AC46E80B848E4012D192E4CE /* LockDistractView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LockDistractView.swift; sourceTree = "<group>"; };
|
||||
BE3D39F325FC7624C1D914D6 /* ITunesSearchService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ITunesSearchService.swift; path = AppRestrict/ITunesSearchService.swift; sourceTree = "<group>"; };
|
||||
BFFBDE92B94E7004F6B642AD /* AppRestrictShieldSettingsVC.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictShieldSettingsVC.swift; path = AppRestrict/AppRestrictShieldSettingsVC.swift; sourceTree = "<group>"; };
|
||||
D0112B7865829D5B6A4A0BD0 /* AppRestrictVC.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictVC.swift; path = AppRestrict/AppRestrictVC.swift; sourceTree = "<group>"; };
|
||||
D64156B4A0E806901AB223FA /* DeviceActivityMonitorExtension.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DeviceActivityMonitorExtension.swift; path = DeviceActivityMonitorExtension/DeviceActivityMonitorExtension.swift; sourceTree = "<group>"; };
|
||||
DA16D49AA46D4F6838340B55 /* Pods-QuickLocation.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-QuickLocation.debug.xcconfig"; path = "Target Support Files/Pods-QuickLocation/Pods-QuickLocation.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
E483B9929C03809ADEDE8341 /* Pods-QuickLocation.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-QuickLocation.release.xcconfig"; path = "Target Support Files/Pods-QuickLocation/Pods-QuickLocation.release.xcconfig"; sourceTree = "<group>"; };
|
||||
EF9D223AB35744F92E0E3E77 /* FamilyActivityPickerHost.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FamilyActivityPickerHost.swift; path = AppRestrict/FamilyActivityPickerHost.swift; sourceTree = "<group>"; };
|
||||
F9CFFB14BA2ECC259151BCDA /* LockDistractVC.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LockDistractVC.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
30CCDF8E2FE3E63B00F5214A /* sound */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = sound;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
30CCDF902FE3E63B00F5214A /* video */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = video;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
30CCE01E2FE3E64700F5214A /* lotties */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = lotties;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
30D87CEF2FDFF52100E958FD /* TTGTagCollectionView */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = TTGTagCollectionView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
|
|
@ -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 = "<group>";
|
||||
};
|
||||
2509CBAFBD63A140B359BD7F /* DeviceActivityMonitorExtension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D64156B4A0E806901AB223FA /* DeviceActivityMonitorExtension.swift */,
|
||||
);
|
||||
name = DeviceActivityMonitorExtension;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2F518AB6587CC227CC515D8C /* AppRestrict */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7AEEA6CEA0CEAAB42C22752B /* AppRestrictManager.swift */,
|
||||
);
|
||||
name = AppRestrict;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
305A74C92FCA8C7000227D26 /* RxMoya */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -981,6 +1129,7 @@
|
|||
305A752C2FCA8C7000227D26 /* URL */,
|
||||
30D87CD52FDF9F1900E958FD /* MQTT */,
|
||||
30C6666A2FFB7C3000E62B25 /* IAPManager */,
|
||||
2F518AB6587CC227CC515D8C /* AppRestrict */,
|
||||
);
|
||||
path = Manager;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1156,6 +1305,8 @@
|
|||
30ACD54A2FF6332200174861 /* PopupWindow */,
|
||||
30EFF3AD2FD7FF1400EB35D4 /* TextInput */,
|
||||
30C6679D2FFB7FEF00E62B25 /* Share */,
|
||||
F49062649CA8E5F9FA2F22A5 /* AppRestrict */,
|
||||
B79BE06D49443FE82A811D5C /* LockDistract */,
|
||||
);
|
||||
path = Section;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1564,15 +1715,6 @@
|
|||
path = SOS;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
55B218B13024B00100784720 /* Explore */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
55B218B13024B00100784721 /* FeatureIntroVC.swift */,
|
||||
55B218B13024B00100784723 /* FeatureIntroView.swift */,
|
||||
);
|
||||
path = Explore;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
30D74AAC2FEA13BD0050EB2C /* Schedule */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -1728,6 +1870,9 @@
|
|||
3E4359092FC48D26003470A5 /* Products */,
|
||||
B07D45692FCECE07570D9B73 /* Pods */,
|
||||
47CD8471BE2146A2656CF27E /* Frameworks */,
|
||||
DA2257EC3F3167F4A6AFE759 /* AppRestrictShared */,
|
||||
2509CBAFBD63A140B359BD7F /* DeviceActivityMonitorExtension */,
|
||||
2234314E8DD57630628CC5C3 /* ShieldConfigurationExtension */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
|
|
@ -1735,6 +1880,8 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
3E4359082FC48D26003470A5 /* QuickLocation.app */,
|
||||
98E91E51811235EAD0985421 /* DeviceActivityMonitorExtension.appex */,
|
||||
0C47E0012491DB1371FD5E53 /* ShieldConfigurationExtension.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -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 = "<group>";
|
||||
|
|
@ -1774,6 +1927,16 @@
|
|||
path = TodayTrackDetail;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
55B217DE302301000078473D /* PhoneReportDetail */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
55B217D73023010000784736 /* PhoneReportDetailVC.swift */,
|
||||
55B217D93023010000784738 /* PhoneReportDetailView.swift */,
|
||||
55B217DB302301000078473A /* PhoneReportDetailViewModel.swift */,
|
||||
);
|
||||
path = PhoneReportDetail;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
55B218A13024A00100784710 /* About */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -1801,14 +1964,21 @@
|
|||
path = MemberInfo;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
74B9CD2ABDBC4C8CE3107CDE /* iOS */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
226CA98495C2884EBB1D1372 /* Foundation.framework */,
|
||||
);
|
||||
name = iOS;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B07D45692FCECE07570D9B73 /* Pods */ = {
|
||||
|
|
@ -1820,6 +1990,40 @@
|
|||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B79BE06D49443FE82A811D5C /* LockDistract */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F9CFFB14BA2ECC259151BCDA /* LockDistractVC.swift */,
|
||||
AC46E80B848E4012D192E4CE /* LockDistractView.swift */,
|
||||
);
|
||||
path = LockDistract;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DA2257EC3F3167F4A6AFE759 /* AppRestrictShared */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1AB29D0618C4BE76F2CA5261 /* AppRestrictShared.swift */,
|
||||
);
|
||||
name = AppRestrictShared;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F49062649CA8E5F9FA2F22A5 /* AppRestrict */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
47922567FCD34DBE9186F8FD /* AppCatalogStore.swift */,
|
||||
89519661B70A0ADEC2A76032 /* AppRestrictCell.swift */,
|
||||
D0112B7865829D5B6A4A0BD0 /* AppRestrictVC.swift */,
|
||||
787F87DD1CEBED1A4E1E9FC7 /* AppRestrictView.swift */,
|
||||
BFFBDE92B94E7004F6B642AD /* AppRestrictShieldSettingsVC.swift */,
|
||||
EF9D223AB35744F92E0E3E77 /* FamilyActivityPickerHost.swift */,
|
||||
BE3D39F325FC7624C1D914D6 /* ITunesSearchService.swift */,
|
||||
6BAE595D39202A2958737BD3 /* SelectActivityVC.swift */,
|
||||
3C79865C3ED165AFDD091C29 /* app_catalog.json */,
|
||||
5E5F5DC8694A001BF0C47147 /* PairGuideStepsView.swift */,
|
||||
);
|
||||
name = AppRestrict;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* 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 */;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : { "author" : "xcode", "version" : 1 },
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
8
QuickLocation/Assets.xcassets/AppRestrict/pair_action_delete.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"images" : [
|
||||
{ "idiom" : "universal", "scale" : "1x" },
|
||||
{ "filename" : "pair_action_delete@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||
{ "filename" : "pair_action_delete@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||
],
|
||||
"info" : { "author" : "xcode", "version" : 1 }
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/AppRestrict/pair_action_delete.imageset/pair_action_delete@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 867 B |
BIN
QuickLocation/Assets.xcassets/AppRestrict/pair_action_delete.imageset/pair_action_delete@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
8
QuickLocation/Assets.xcassets/AppRestrict/pair_action_link.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"images" : [
|
||||
{ "idiom" : "universal", "scale" : "1x" },
|
||||
{ "filename" : "pair_action_link@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||
{ "filename" : "pair_action_link@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||
],
|
||||
"info" : { "author" : "xcode", "version" : 1 }
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/AppRestrict/pair_action_link.imageset/pair_action_link@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
QuickLocation/Assets.xcassets/AppRestrict/pair_action_link.imageset/pair_action_link@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
8
QuickLocation/Assets.xcassets/AppRestrict/pair_badge_check.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"images" : [
|
||||
{ "idiom" : "universal", "scale" : "1x" },
|
||||
{ "filename" : "pair_badge_check@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||
{ "filename" : "pair_badge_check@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||
],
|
||||
"info" : { "author" : "xcode", "version" : 1 }
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/AppRestrict/pair_badge_check.imageset/pair_badge_check@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 493 B |
BIN
QuickLocation/Assets.xcassets/AppRestrict/pair_badge_check.imageset/pair_badge_check@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
8
QuickLocation/Assets.xcassets/AppRestrict/pair_link_active.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"images" : [
|
||||
{ "idiom" : "universal", "scale" : "1x" },
|
||||
{ "filename" : "pair_link_active@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||
{ "filename" : "pair_link_active@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||
],
|
||||
"info" : { "author" : "xcode", "version" : 1 }
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/AppRestrict/pair_link_active.imageset/pair_link_active@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
QuickLocation/Assets.xcassets/AppRestrict/pair_link_active.imageset/pair_link_active@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
8
QuickLocation/Assets.xcassets/AppRestrict/pair_link_inactive.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"images" : [
|
||||
{ "idiom" : "universal", "scale" : "1x" },
|
||||
{ "filename" : "pair_link_inactive@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||
{ "filename" : "pair_link_inactive@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||
],
|
||||
"info" : { "author" : "xcode", "version" : 1 }
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/AppRestrict/pair_link_inactive.imageset/pair_link_inactive@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
QuickLocation/Assets.xcassets/AppRestrict/pair_link_inactive.imageset/pair_link_inactive@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 678 B After Width: | Height: | Size: 830 B |
|
Before Width: | Height: | Size: 869 B After Width: | Height: | Size: 1.9 KiB |
|
|
@ -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
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/Group/join_hero_bg.imageset/join_hero_bg@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
QuickLocation/Assets.xcassets/Group/join_hero_bg.imageset/join_hero_bg@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 11 KiB |
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
8
QuickLocation/Assets.xcassets/LockDistract/app_unknown.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"images" : [
|
||||
{ "idiom" : "universal", "scale" : "1x" },
|
||||
{ "filename" : "app_unknown@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||
{ "filename" : "app_unknown@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||
],
|
||||
"info" : { "author" : "xcode", "version" : 1 }
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/LockDistract/app_unknown.imageset/app_unknown@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
QuickLocation/Assets.xcassets/LockDistract/app_unknown.imageset/app_unknown@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
8
QuickLocation/Assets.xcassets/LockDistract/arrow_right.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"images" : [
|
||||
{ "idiom" : "universal", "scale" : "1x" },
|
||||
{ "filename" : "arrow_right@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||
{ "filename" : "arrow_right@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||
],
|
||||
"info" : { "author" : "xcode", "version" : 1 }
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/LockDistract/arrow_right.imageset/arrow_right@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 302 B |
BIN
QuickLocation/Assets.xcassets/LockDistract/arrow_right.imageset/arrow_right@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 465 B |
8
QuickLocation/Assets.xcassets/LockDistract/section_star.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"images" : [
|
||||
{ "idiom" : "universal", "scale" : "1x" },
|
||||
{ "filename" : "section_star@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||
{ "filename" : "section_star@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||
],
|
||||
"info" : { "author" : "xcode", "version" : 1 }
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/LockDistract/section_star.imageset/section_star@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 433 B |
BIN
QuickLocation/Assets.xcassets/LockDistract/section_star.imageset/section_star@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 772 B |
8
QuickLocation/Assets.xcassets/LockDistract/wallpaper_1.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"images" : [
|
||||
{ "idiom" : "universal", "scale" : "1x" },
|
||||
{ "filename" : "wallpaper_1@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||
{ "filename" : "wallpaper_1@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||
],
|
||||
"info" : { "author" : "xcode", "version" : 1 }
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/LockDistract/wallpaper_1.imageset/wallpaper_1@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
QuickLocation/Assets.xcassets/LockDistract/wallpaper_1.imageset/wallpaper_1@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 66 KiB |
8
QuickLocation/Assets.xcassets/LockDistract/wallpaper_2.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"images" : [
|
||||
{ "idiom" : "universal", "scale" : "1x" },
|
||||
{ "filename" : "wallpaper_2@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||
{ "filename" : "wallpaper_2@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||
],
|
||||
"info" : { "author" : "xcode", "version" : 1 }
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/LockDistract/wallpaper_2.imageset/wallpaper_2@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
QuickLocation/Assets.xcassets/LockDistract/wallpaper_2.imageset/wallpaper_2@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 90 KiB |
8
QuickLocation/Assets.xcassets/LockDistract/wallpaper_3.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"images" : [
|
||||
{ "idiom" : "universal", "scale" : "1x" },
|
||||
{ "filename" : "wallpaper_3@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||
{ "filename" : "wallpaper_3@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||
],
|
||||
"info" : { "author" : "xcode", "version" : 1 }
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/LockDistract/wallpaper_3.imageset/wallpaper_3@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
QuickLocation/Assets.xcassets/LockDistract/wallpaper_3.imageset/wallpaper_3@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 84 KiB |
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,140 @@
|
|||
//
|
||||
// AppRestrictManager.swift
|
||||
// QuickLocation
|
||||
//
|
||||
|
||||
import DeviceActivity
|
||||
import FamilyControls
|
||||
import Foundation
|
||||
import ManagedSettings
|
||||
import UIKit
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
final class AppRestrictManager {
|
||||
static let shared = AppRestrictManager()
|
||||
|
||||
private let center = AuthorizationCenter.shared
|
||||
private let activityCenter = DeviceActivityCenter()
|
||||
private let activityName = DeviceActivityName(AppRestrictShared.activityName)
|
||||
|
||||
private init() {}
|
||||
|
||||
var authorizationStatus: AuthorizationStatus {
|
||||
center.authorizationStatus
|
||||
}
|
||||
|
||||
var isAuthorized: Bool {
|
||||
center.authorizationStatus == .approved
|
||||
}
|
||||
|
||||
var selection: FamilyActivitySelection {
|
||||
get { AppRestrictSharedStore.selection }
|
||||
set {
|
||||
AppRestrictSharedStore.selection = newValue
|
||||
// Drop enabled tokens that are no longer in selection
|
||||
let apps = newValue.applicationTokens
|
||||
AppRestrictSharedStore.enabledTokens = AppRestrictSharedStore.enabledTokens.intersection(apps)
|
||||
refreshMonitoringAndShield()
|
||||
}
|
||||
}
|
||||
|
||||
var applicationTokens: [ApplicationToken] {
|
||||
Array(selection.applicationTokens)
|
||||
}
|
||||
|
||||
var enabledTokens: Set<ApplicationToken> {
|
||||
get { AppRestrictSharedStore.enabledTokens }
|
||||
set {
|
||||
AppRestrictSharedStore.enabledTokens = newValue
|
||||
refreshMonitoringAndShield()
|
||||
}
|
||||
}
|
||||
|
||||
func requestAuthorization() async throws {
|
||||
try await center.requestAuthorization(for: .individual)
|
||||
}
|
||||
|
||||
func mergeSelection(_ incoming: FamilyActivitySelection) {
|
||||
var current = selection
|
||||
current.applicationTokens.formUnion(incoming.applicationTokens)
|
||||
current.categoryTokens.formUnion(incoming.categoryTokens)
|
||||
current.webDomainTokens.formUnion(incoming.webDomainTokens)
|
||||
selection = current
|
||||
}
|
||||
|
||||
func isEnabled(_ token: ApplicationToken) -> Bool {
|
||||
enabledTokens.contains(token)
|
||||
}
|
||||
|
||||
func setEnabled(_ token: ApplicationToken, enabled: Bool) {
|
||||
var set = enabledTokens
|
||||
if enabled {
|
||||
set.insert(token)
|
||||
} else {
|
||||
set.remove(token)
|
||||
}
|
||||
enabledTokens = set
|
||||
}
|
||||
|
||||
func catalogId(for token: ApplicationToken) -> String? {
|
||||
AppRestrictSharedStore.catalogId(for: token)
|
||||
}
|
||||
|
||||
func link(catalogId: String, token: ApplicationToken, displayName: String? = nil, iconURL: String? = nil) {
|
||||
AppRestrictSharedStore.setLink(
|
||||
catalogId: catalogId,
|
||||
token: token,
|
||||
displayName: displayName,
|
||||
iconURL: iconURL
|
||||
)
|
||||
}
|
||||
|
||||
func catalogItem(for token: ApplicationToken) -> AppCatalogItem? {
|
||||
guard let record = AppRestrictSharedStore.linkRecord(for: token) else { return nil }
|
||||
return AppCatalogStore.resolve(link: record)
|
||||
}
|
||||
|
||||
func unlink(_ token: ApplicationToken) {
|
||||
AppRestrictSharedStore.removeLink(for: token)
|
||||
}
|
||||
|
||||
func removeApplication(_ token: ApplicationToken) {
|
||||
unlink(token)
|
||||
var set = enabledTokens
|
||||
set.remove(token)
|
||||
enabledTokens = set
|
||||
|
||||
var current = selection
|
||||
current.applicationTokens.remove(token)
|
||||
selection = current
|
||||
}
|
||||
|
||||
var shieldConfig: AppRestrictShieldConfig {
|
||||
get { AppRestrictSharedStore.shieldConfig }
|
||||
set { AppRestrictSharedStore.shieldConfig = newValue }
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func saveCustomShieldImage(_ image: UIImage) -> Bool {
|
||||
AppRestrictSharedStore.saveCustomImage(image)
|
||||
}
|
||||
|
||||
func refreshMonitoringAndShield() {
|
||||
let tokens = enabledTokens
|
||||
AppRestrictSharedStore.applyShield(for: tokens)
|
||||
activityCenter.stopMonitoring([activityName])
|
||||
guard !tokens.isEmpty else { return }
|
||||
// Near-daily schedule so Monitor can re-apply after reboot / schedule boundaries.
|
||||
let schedule = DeviceActivitySchedule(
|
||||
intervalStart: DateComponents(hour: 0, minute: 0),
|
||||
intervalEnd: DateComponents(hour: 23, minute: 59),
|
||||
repeats: true
|
||||
)
|
||||
do {
|
||||
try activityCenter.startMonitoring(activityName, during: schedule)
|
||||
} catch {
|
||||
// Shield already applied above; monitoring is best-effort.
|
||||
print("[AppRestrict] startMonitoring failed: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,5 +12,11 @@
|
|||
</array>
|
||||
<key>com.apple.developer.networking.wifi-info</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.family-controls</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.cn.zuomeng.jisuloca</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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?()
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
//
|
||||
// FamilyActivityPickerHost.swift
|
||||
// QuickLocation
|
||||
//
|
||||
|
||||
import FamilyControls
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import ManagedSettings
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
private struct FamilyActivityPickerSheet: View {
|
||||
@State private var selection: FamilyActivitySelection
|
||||
@State private var isPresented = true
|
||||
let onComplete: (FamilyActivitySelection) -> Void
|
||||
|
||||
init(initial: FamilyActivitySelection, onComplete: @escaping (FamilyActivitySelection) -> Void) {
|
||||
_selection = State(initialValue: initial)
|
||||
self.onComplete = onComplete
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Color.clear
|
||||
.familyActivityPicker(isPresented: $isPresented, selection: $selection)
|
||||
.onChange(of: isPresented) { presented in
|
||||
if !presented {
|
||||
onComplete(selection)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
final class FamilyActivityPickerPresenter: UIViewController {
|
||||
private let initial: FamilyActivitySelection
|
||||
var onComplete: ((FamilyActivitySelection) -> Void)?
|
||||
|
||||
init(selection: FamilyActivitySelection = FamilyActivitySelection()) {
|
||||
self.initial = selection
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
modalPresentationStyle = .overFullScreen
|
||||
view.backgroundColor = .clear
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
guard children.isEmpty else { return }
|
||||
let root = FamilyActivityPickerSheet(initial: initial) { [weak self] selection in
|
||||
self?.onComplete?(selection)
|
||||
self?.dismiss(animated: false)
|
||||
}
|
||||
let host = UIHostingController(rootView: root)
|
||||
host.view.backgroundColor = .clear
|
||||
addChild(host)
|
||||
view.addSubview(host.view)
|
||||
host.view.frame = .zero
|
||||
host.didMove(toParent: self)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
struct ApplicationTokenLabelView: View {
|
||||
let token: ApplicationToken
|
||||
|
||||
var body: some View {
|
||||
Label(token)
|
||||
.labelStyle(.titleAndIcon)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
struct ApplicationTokenIconView: View {
|
||||
let token: ApplicationToken
|
||||
|
||||
var body: some View {
|
||||
Label(token)
|
||||
.labelStyle(.iconOnly)
|
||||
.scaleEffect(2.4)
|
||||
.frame(width: 40, height: 40)
|
||||
.clipped()
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
final class ApplicationTokenIconHostingView: UIView {
|
||||
private let hosting: UIHostingController<ApplicationTokenIconView>
|
||||
|
||||
init(token: ApplicationToken) {
|
||||
hosting = UIHostingController(rootView: ApplicationTokenIconView(token: token))
|
||||
super.init(frame: .zero)
|
||||
hosting.view.backgroundColor = .clear
|
||||
addSubview(hosting.view)
|
||||
hosting.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
hosting.view.topAnchor.constraint(equalTo: topAnchor),
|
||||
hosting.view.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
hosting.view.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
hosting.view.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
func update(token: ApplicationToken) {
|
||||
hosting.rootView = ApplicationTokenIconView(token: token)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
final class ApplicationTokenLabelHostingView: UIView {
|
||||
private let hosting: UIHostingController<ApplicationTokenLabelView>
|
||||
|
||||
init(token: ApplicationToken) {
|
||||
hosting = UIHostingController(rootView: ApplicationTokenLabelView(token: token))
|
||||
super.init(frame: .zero)
|
||||
hosting.view.backgroundColor = .clear
|
||||
addSubview(hosting.view)
|
||||
hosting.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
hosting.view.topAnchor.constraint(equalTo: topAnchor),
|
||||
hosting.view.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
hosting.view.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
hosting.view.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
func update(token: ApplicationToken) {
|
||||
hosting.rootView = ApplicationTokenLabelView(token: token)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
//
|
||||
// ITunesSearchService.swift
|
||||
// QuickLocation
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ITunesSearchError: Error {
|
||||
case invalidURL
|
||||
case invalidResponse
|
||||
}
|
||||
|
||||
enum ITunesSearchService {
|
||||
private struct SearchResponse: Decodable {
|
||||
let results: [ITunesAppResult]
|
||||
}
|
||||
|
||||
private struct ITunesAppResult: Decodable {
|
||||
let trackId: Int
|
||||
let trackName: String
|
||||
let artworkUrl100: String?
|
||||
let artworkUrl512: String?
|
||||
}
|
||||
|
||||
static func search(term: String, limit: Int = 25) async throws -> [AppCatalogItem] {
|
||||
let trimmed = term.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return [] }
|
||||
|
||||
var components = URLComponents(string: "https://itunes.apple.com/search")
|
||||
components?.queryItems = [
|
||||
URLQueryItem(name: "term", value: trimmed),
|
||||
URLQueryItem(name: "entity", value: "software"),
|
||||
URLQueryItem(name: "country", value: "cn"),
|
||||
URLQueryItem(name: "limit", value: "\(limit)")
|
||||
]
|
||||
guard let url = components?.url else { throw ITunesSearchError.invalidURL }
|
||||
|
||||
let (data, response) = try await URLSession.shared.data(from: url)
|
||||
guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode) else {
|
||||
throw ITunesSearchError.invalidResponse
|
||||
}
|
||||
|
||||
let decoded = try JSONDecoder().decode(SearchResponse.self, from: data)
|
||||
return decoded.results.map { result in
|
||||
let iconURL = result.artworkUrl512 ?? result.artworkUrl100
|
||||
return AppCatalogItem(
|
||||
id: "itunes:\(result.trackId)",
|
||||
name: result.trackName,
|
||||
icon: "",
|
||||
keywords: [],
|
||||
iconURL: iconURL
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
static func mergedResults(local: [AppCatalogItem], remote: [AppCatalogItem]) -> [AppCatalogItem] {
|
||||
var seen = Set<String>()
|
||||
var merged: [AppCatalogItem] = []
|
||||
for item in local + remote {
|
||||
guard !seen.contains(item.id) else { continue }
|
||||
seen.insert(item.id)
|
||||
merged.append(item)
|
||||
}
|
||||
return merged
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
//
|
||||
// PairGuideStepsView.swift
|
||||
// QuickLocation
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class PairGuideStepsView: UIView {
|
||||
private struct Step {
|
||||
let number: String
|
||||
let title: String
|
||||
let color: UIColor
|
||||
}
|
||||
|
||||
private let steps: [Step] = [
|
||||
Step(number: "1", title: "新增配对应用", color: UIColor(hexStr: "#8B5CF6")),
|
||||
Step(number: "2", title: "选择相同应用", color: UIColor(hexStr: "#16B3FF")),
|
||||
Step(number: "3", title: "完成配对", color: UIColor(hexStr: "#22C55E"))
|
||||
]
|
||||
|
||||
private let circleSize: CGFloat = 28
|
||||
private let dashLayer = CAShapeLayer()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .clear
|
||||
dashLayer.strokeColor = UIColor(hexStr: "#D1D5DB").cgColor
|
||||
dashLayer.lineWidth = 1
|
||||
dashLayer.lineDashPattern = [3, 3]
|
||||
dashLayer.fillColor = UIColor.clear.cgColor
|
||||
layer.addSublayer(dashLayer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
CGSize(width: UIView.noIntrinsicMetric, height: 72)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
subviews.forEach { $0.removeFromSuperview() }
|
||||
|
||||
let columnWidth = bounds.width / 3
|
||||
let circleY = circleSize / 2 + 2
|
||||
let centers = (0..<steps.count).map { columnWidth * (CGFloat($0) + 0.5) }
|
||||
|
||||
let path = UIBezierPath()
|
||||
path.move(to: CGPoint(x: centers.first ?? 0, y: circleY))
|
||||
path.addLine(to: CGPoint(x: centers.last ?? bounds.width, y: circleY))
|
||||
dashLayer.path = path.cgPath
|
||||
dashLayer.frame = bounds
|
||||
|
||||
for (index, step) in steps.enumerated() {
|
||||
let centerX = centers[index]
|
||||
|
||||
let circle = UILabel(frame: CGRect(
|
||||
x: centerX - circleSize / 2,
|
||||
y: 0,
|
||||
width: circleSize,
|
||||
height: circleSize
|
||||
))
|
||||
circle.text = step.number
|
||||
circle.font = FontManager.boboBold(14)
|
||||
circle.textColor = .white
|
||||
circle.textAlignment = .center
|
||||
circle.backgroundColor = step.color
|
||||
circle.layer.cornerRadius = circleSize / 2
|
||||
circle.clipsToBounds = true
|
||||
addSubview(circle)
|
||||
|
||||
let label = UILabel()
|
||||
label.text = step.title
|
||||
label.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
label.textColor = UIColor(hexStr: "#293445")
|
||||
label.textAlignment = .center
|
||||
label.frame = CGRect(
|
||||
x: columnWidth * CGFloat(index) + 4,
|
||||
y: circleSize + 10,
|
||||
width: columnWidth - 8,
|
||||
height: 34
|
||||
)
|
||||
addSubview(label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
//
|
||||
// SelectActivityVC.swift
|
||||
// QuickLocation
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
import Kingfisher
|
||||
|
||||
final class SelectActivityCell: UITableViewCell {
|
||||
static let reuseId = "SelectActivityCell"
|
||||
|
||||
private let iconView = UIImageView()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
iconView.contentMode = .scaleAspectFill
|
||||
iconView.layer.cornerRadius = 10
|
||||
iconView.clipsToBounds = true
|
||||
contentView.addSubview(iconView)
|
||||
iconView.layoutChain.left(16).centerY().width(40).height(40)
|
||||
textLabel?.layoutChain.leftToRightOfView(iconView, offset: 12).centerY().right(16)
|
||||
selectionStyle = .default
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
func configure(item: AppCatalogItem) {
|
||||
textLabel?.text = item.name
|
||||
textLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
textLabel?.textColor = UIColor(hexStr: "#293445")
|
||||
AppCatalogStore.setIcon(for: iconView, item: item)
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
iconView.kf.cancelDownloadTask()
|
||||
iconView.image = nil
|
||||
}
|
||||
}
|
||||
|
||||
final class SelectActivityVC: BaseViewController {
|
||||
var onSelect: ((AppCatalogItem) -> Void)?
|
||||
|
||||
private let searchField = UITextField()
|
||||
private let tableView = UITableView(frame: .zero, style: .plain)
|
||||
private let activityIndicator = UIActivityIndicatorView(style: .medium)
|
||||
private var items: [AppCatalogItem] = AppCatalogStore.sharedItems
|
||||
private var searchTask: Task<Void, Never>?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .white
|
||||
title = "选取应用"
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "取消", style: .plain, target: self, action: #selector(cancel))
|
||||
|
||||
searchField.placeholder = "搜索应用"
|
||||
searchField.borderStyle = .roundedRect
|
||||
searchField.font = .systemFont(ofSize: 15)
|
||||
searchField.clearButtonMode = .whileEditing
|
||||
searchField.returnKeyType = .search
|
||||
searchField.addTarget(self, action: #selector(searchChanged), for: .editingChanged)
|
||||
view.addSubview(searchField)
|
||||
searchField.layoutChain.top(12).left(16).right(16).height(40)
|
||||
|
||||
activityIndicator.hidesWhenStopped = true
|
||||
view.addSubview(activityIndicator)
|
||||
activityIndicator.layoutChain.centerX().topToBottomOfView(searchField, offset: 8)
|
||||
|
||||
tableView.register(SelectActivityCell.self, forCellReuseIdentifier: SelectActivityCell.reuseId)
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
tableView.rowHeight = 56
|
||||
view.addSubview(tableView)
|
||||
tableView.layoutChain.topToBottomOfView(searchField, offset: 12).edgesHorzontal().bottom()
|
||||
}
|
||||
|
||||
@objc private func cancel() {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
|
||||
@objc private func searchChanged() {
|
||||
let query = searchField.text ?? ""
|
||||
searchTask?.cancel()
|
||||
|
||||
let trimmed = query.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if trimmed.isEmpty {
|
||||
activityIndicator.stopAnimating()
|
||||
items = AppCatalogStore.sharedItems
|
||||
tableView.reloadData()
|
||||
return
|
||||
}
|
||||
|
||||
let localHits = AppCatalogStore.search(query)
|
||||
items = localHits
|
||||
tableView.reloadData()
|
||||
activityIndicator.startAnimating()
|
||||
|
||||
searchTask = Task { [weak self] in
|
||||
try? await Task.sleep(nanoseconds: 300_000_000)
|
||||
guard let self, !Task.isCancelled else { return }
|
||||
do {
|
||||
let remote = try await ITunesSearchService.search(term: trimmed)
|
||||
guard !Task.isCancelled else { return }
|
||||
await MainActor.run {
|
||||
self.activityIndicator.stopAnimating()
|
||||
self.items = ITunesSearchService.mergedResults(local: localHits, remote: remote)
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.activityIndicator.stopAnimating()
|
||||
if self.items.isEmpty {
|
||||
self.items = localHits
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
DLToast.show(text: "搜索失败,请检查网络")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SelectActivityVC: UITableViewDataSource, UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
items.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: SelectActivityCell.reuseId, for: indexPath) as! SelectActivityCell
|
||||
cell.configure(item: items[indexPath.row])
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let item = items[indexPath.row]
|
||||
onSelect?(item)
|
||||
dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
|
|
@ -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": ["王者", "荣耀"]
|
||||
}
|
||||
]
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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: "飞鸽传书",
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
rootView.iconCarousel.onSelectIndex = { [weak self] index in
|
||||
self?.viewModel.iconIndex = index
|
||||
}
|
||||
self.navigationController?.pushViewController(vc, animated: true)
|
||||
}.disposed(by: disposeBag)
|
||||
|
||||
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<GroupTagListSectionModel> = {
|
||||
RxCollectionViewSectionedReloadDataSource<GroupTagListSectionModel> { datasource, collectionView, indexPath, item in
|
||||
let cell: TagCell = collectionView.dequeueReusableCell(for: indexPath)
|
||||
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])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
.topToBottomOfView(groupContentInputView, offset: 18)
|
||||
.left(24)
|
||||
|
||||
tagView.layoutChain
|
||||
.topToBottomOfView(tagTitleLab, offset: 10)
|
||||
.edgesHorzontal(15)
|
||||
.height(64)
|
||||
.topToBottomOfView(tagTitleLab, offset: 12)
|
||||
.edgesHorzontal(20)
|
||||
.height(28)
|
||||
.bottom(24)
|
||||
|
||||
tipsLab.layoutChain
|
||||
.topToBottomOfView(tagView, offset: 10)
|
||||
.left(15)
|
||||
.bottom()
|
||||
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
|
||||
}()
|
||||
|
||||
let icon = UIImageView()
|
||||
icon.image = UIImage(named: "Group/edit")
|
||||
view.addSubview(icon)
|
||||
icon.layoutChain
|
||||
.right(15)
|
||||
.edgesVertical(20)
|
||||
.width(20)
|
||||
.height(20)
|
||||
lazy var iconCarousel: CreateGroupIconCarouselView = {
|
||||
let view = CreateGroupIconCarouselView()
|
||||
return view
|
||||
}()
|
||||
|
||||
let line = UIView()
|
||||
line.backgroundColor = UIColor(hexStr: "#EEEEEE")
|
||||
view.addSubview(line)
|
||||
line.layoutChain
|
||||
.edgesHorzontal(15)
|
||||
.height(0.5)
|
||||
.bottom()
|
||||
lazy var typeNamePlaceholder: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .white
|
||||
view.cornerRadius = 10
|
||||
view.isUserInteractionEnabled = false
|
||||
|
||||
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,119 +393,63 @@ 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
|
||||
|
|
@ -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,7 +533,234 @@ class CreateGroupView: UIView {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - Icon Carousel
|
||||
|
||||
final class CreateGroupIconCarouselView: UIView {
|
||||
|
||||
var onSelectIndex: ((Int) -> Void)?
|
||||
|
||||
private let iconCount = 11
|
||||
private let itemSize: CGFloat = 80
|
||||
private var lastWidth: CGFloat = 0
|
||||
|
||||
var selectedIndex: Int = 1 {
|
||||
didSet {
|
||||
guard oldValue != selectedIndex else { return }
|
||||
onSelectIndex?(selectedIndex)
|
||||
}
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
clipsToBounds = false
|
||||
addSubview(collectionView)
|
||||
collectionView.layoutChain.edges()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
guard bounds.width > 0 else { return }
|
||||
if bounds.width != lastWidth {
|
||||
lastWidth = bounds.width
|
||||
let inset = max(0, (bounds.width - itemSize) / 2)
|
||||
flowLayout.sectionInset = UIEdgeInsets(top: 25, left: inset, bottom: 25, right: inset)
|
||||
collectionView.layoutIfNeeded()
|
||||
scrollToIndex(selectedIndex, animated: false)
|
||||
}
|
||||
applyTransforms()
|
||||
}
|
||||
|
||||
private func scrollToIndex(_ index: Int, animated: Bool) {
|
||||
let item = max(0, min(iconCount - 1, index - 1))
|
||||
collectionView.scrollToItem(at: IndexPath(item: item, section: 0),
|
||||
at: .centeredHorizontally,
|
||||
animated: animated)
|
||||
}
|
||||
|
||||
private func nearestIndex() -> Int {
|
||||
let centerX = collectionView.contentOffset.x + collectionView.bounds.width / 2
|
||||
var nearest = 0
|
||||
var minDist = CGFloat.greatestFiniteMagnitude
|
||||
for i in 0..<iconCount {
|
||||
guard let attr = collectionView.layoutAttributesForItem(at: IndexPath(item: i, section: 0)) else { continue }
|
||||
let dist = abs(attr.center.x - centerX)
|
||||
if dist < minDist {
|
||||
minDist = dist
|
||||
nearest = i
|
||||
}
|
||||
}
|
||||
return nearest
|
||||
}
|
||||
|
||||
private func applyTransforms() {
|
||||
let midX = collectionView.contentOffset.x + collectionView.bounds.width / 2
|
||||
for cell in collectionView.visibleCells {
|
||||
guard let attr = collectionView.layoutAttributesForItem(at: collectionView.indexPath(for: cell) ?? IndexPath()) else { continue }
|
||||
let distance = abs(attr.center.x - midX)
|
||||
let scale = max(0.72, 1.25 - distance / 180)
|
||||
let alpha = max(0.45, 1 - distance / 220)
|
||||
cell.transform = CGAffineTransform(scaleX: scale, y: scale)
|
||||
cell.alpha = alpha
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var flowLayout: UICollectionViewFlowLayout = {
|
||||
let layout = UICollectionViewFlowLayout()
|
||||
layout.scrollDirection = .horizontal
|
||||
layout.itemSize = CGSize(width: itemSize, height: itemSize)
|
||||
layout.minimumLineSpacing = 18
|
||||
layout.minimumInteritemSpacing = 0
|
||||
return layout
|
||||
}()
|
||||
|
||||
private lazy var collectionView: UICollectionView = {
|
||||
let cv = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
|
||||
cv.backgroundColor = .clear
|
||||
cv.showsHorizontalScrollIndicator = false
|
||||
cv.decelerationRate = .fast
|
||||
cv.clipsToBounds = false
|
||||
cv.register(CreateGroupIconCell.self)
|
||||
cv.dataSource = self
|
||||
cv.delegate = self
|
||||
return cv
|
||||
}()
|
||||
}
|
||||
|
||||
extension CreateGroupIconCarouselView: UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
iconCount
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell = collectionView.dequeueReusableCell(for: indexPath) as CreateGroupIconCell
|
||||
cell.configure(UIImage(named: "GroupIcon/\(indexPath.item + 1)"))
|
||||
return cell
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
||||
applyTransforms()
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
selectedIndex = indexPath.item + 1
|
||||
collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
applyTransforms()
|
||||
}
|
||||
|
||||
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||
let proposedCenter = targetContentOffset.pointee.x + scrollView.bounds.width / 2
|
||||
var nearest = 0
|
||||
var minDist = CGFloat.greatestFiniteMagnitude
|
||||
for i in 0..<iconCount {
|
||||
guard let attr = collectionView.layoutAttributesForItem(at: IndexPath(item: i, section: 0)) else { continue }
|
||||
let dist = abs(attr.center.x - proposedCenter)
|
||||
if dist < minDist {
|
||||
minDist = dist
|
||||
nearest = i
|
||||
}
|
||||
}
|
||||
if let attr = collectionView.layoutAttributesForItem(at: IndexPath(item: nearest, section: 0)) {
|
||||
targetContentOffset.pointee.x = attr.center.x - scrollView.bounds.width / 2
|
||||
}
|
||||
selectedIndex = nearest + 1
|
||||
}
|
||||
|
||||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
selectedIndex = nearestIndex() + 1
|
||||
applyTransforms()
|
||||
}
|
||||
|
||||
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
|
||||
applyTransforms()
|
||||
}
|
||||
}
|
||||
|
||||
final class CreateGroupIconCell: UICollectionViewCell {
|
||||
private let imageView: UIImageView = {
|
||||
let view = UIImageView()
|
||||
view.contentMode = .scaleAspectFill
|
||||
view.clipsToBounds = true
|
||||
view.cornerRadius = 18
|
||||
return view
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
contentView.addSubview(imageView)
|
||||
imageView.layoutChain.edges()
|
||||
contentView.clipsToBounds = false
|
||||
clipsToBounds = false
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func configure(_ image: UIImage?) {
|
||||
imageView.image = image
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
transform = .identity
|
||||
alpha = 1
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Tag Flow Layout
|
||||
|
||||
final class CreateGroupTagFlowLayout: UICollectionViewFlowLayout {
|
||||
private var alignedAttrs: [IndexPath: UICollectionViewLayoutAttributes] = [:]
|
||||
|
||||
override func prepare() {
|
||||
super.prepare()
|
||||
alignedAttrs.removeAll()
|
||||
let rect = CGRect(origin: .zero, size: collectionViewContentSize)
|
||||
guard let original = super.layoutAttributesForElements(in: rect) else { return }
|
||||
let attrs = original.map { $0.copy() as! UICollectionViewLayoutAttributes }
|
||||
leftAlign(attrs)
|
||||
attrs.forEach { alignedAttrs[$0.indexPath] = $0 }
|
||||
}
|
||||
|
||||
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
|
||||
alignedAttrs.values.filter { $0.frame.intersects(rect) }
|
||||
}
|
||||
|
||||
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
|
||||
alignedAttrs[indexPath] ?? super.layoutAttributesForItem(at: indexPath)
|
||||
}
|
||||
|
||||
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
|
||||
collectionView?.bounds.width != newBounds.width
|
||||
}
|
||||
|
||||
private func leftAlign(_ attrs: [UICollectionViewLayoutAttributes]) {
|
||||
var x = sectionInset.left
|
||||
var y = sectionInset.top
|
||||
var lineHeight: CGFloat = 0
|
||||
let maxX = (collectionView?.bounds.width ?? 0) - sectionInset.right
|
||||
for attr in attrs where attr.representedElementCategory == .cell {
|
||||
if x > sectionInset.left && x + attr.frame.width > maxX {
|
||||
x = sectionInset.left
|
||||
y += lineHeight + minimumLineSpacing
|
||||
lineHeight = 0
|
||||
}
|
||||
attr.frame.origin = CGPoint(x: x, y: y)
|
||||
x += attr.frame.width + minimumInteritemSpacing
|
||||
lineHeight = max(lineHeight, attr.frame.height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - TagCell
|
||||
|
||||
final class TagCell: UICollectionViewCell {
|
||||
static let reuseId = "TagCell"
|
||||
|
||||
|
|
@ -395,14 +771,26 @@ final class TagCell: UICollectionViewCell {
|
|||
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()
|
||||
}
|
||||
|
||||
|
|
@ -410,12 +798,41 @@ final class TagCell: UICollectionViewCell {
|
|||
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()
|
||||
|
|
@ -424,10 +841,12 @@ final class TagCell: UICollectionViewCell {
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,15 @@ import SwiftyUserDefaults
|
|||
typealias GroupTagListSectionModel = SectionModel<String, String>
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ class JoinGroupVC: BaseViewController {
|
|||
|
||||
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()
|
||||
|
|
@ -31,6 +31,10 @@ class JoinGroupVC: BaseViewController {
|
|||
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
|
||||
|
|
|
|||
|
|
@ -16,7 +16,14 @@ class JoinGroupView: UIView {
|
|||
var numberBtns: [UIButton] = []
|
||||
|
||||
var inviteCode: String = "" {
|
||||
didSet {
|
||||
didSet { updateCodeBoxes() }
|
||||
}
|
||||
|
||||
@objc func numAction(button: UIButton) {
|
||||
textField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
private func updateCodeBoxes() {
|
||||
for btn in numberBtns {
|
||||
btn.setTitle("", for: .normal)
|
||||
btn.isSelected = false
|
||||
|
|
@ -25,16 +32,13 @@ class JoinGroupView: UIView {
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
numberBtns[index].isSelected = true
|
||||
}
|
||||
|
||||
@objc func numAction(button: UIButton) {
|
||||
textField.becomeFirstResponder()
|
||||
let cursorIndex = characters.count
|
||||
if cursorIndex < numberBtns.count {
|
||||
numberBtns[cursorIndex].isSelected = true
|
||||
}
|
||||
}
|
||||
|
||||
private func setupRx() {
|
||||
|
|
@ -43,223 +47,234 @@ class JoinGroupView: UIView {
|
|||
.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
|
||||
}()
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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" : "")
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>ShieldConfiguration</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.ManagedSettingsUI.shield-configuration-service</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).ShieldConfigurationExtension</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.family-controls</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.cn.zuomeng.jisuloca</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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'
|
||||
|
|
@ -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'
|
||||