diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 40469d772..581f97c36 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,4 +15,4 @@ jobs: with: xcode-version: latest-stable - name: Test SDK - run: make test + run: make test \ No newline at end of file diff --git a/.swiftlint.yml b/.swiftlint.yml index fc619f0f6..f3bccd5f4 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,6 +1,7 @@ excluded: # case-sensitive paths to ignore during linting. Takes precedence over `included` - PostHogExample - PostHogExampleWithSPM + - PostHogExampleAutocapture - PostHogTests - PostHog/Utils/ReadWriteLock.swift - PostHog/Utils/Reachability.swift @@ -19,6 +20,11 @@ file_length: warning: 1000 error: 1200 +identifier_name: + excluded: + - id + - ^ph_.*$ + function_body_length: - 1000 # warning - 1200 # error diff --git a/CHANGELOG.md b/CHANGELOG.md index e9f866ff3..b85121188 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## Next +- add autocapture support for UIKit ([#224](https://github.com/PostHog/posthog-ios/pull/224)) + ## 3.14.2 - 2024-11-08 - fix issue with resetting accent color in SwiftUI app ([#238](https://github.com/PostHog/posthog-ios/pull/238)) diff --git a/PostHog.xcodeproj/project.pbxproj b/PostHog.xcodeproj/project.pbxproj index 8fbf97cae..fd3966a7a 100644 --- a/PostHog.xcodeproj/project.pbxproj +++ b/PostHog.xcodeproj/project.pbxproj @@ -119,6 +119,12 @@ 69F5181A2BAC81FC00F52C14 /* UITextInputTraits+Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69F518192BAC81FC00F52C14 /* UITextInputTraits+Util.swift */; }; 69F518382BB2BA0100F52C14 /* PostHogSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69F518372BB2BA0100F52C14 /* PostHogSwizzler.swift */; }; 69F5183A2BB2BA8300F52C14 /* UIApplicationTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69F518392BB2BA8300F52C14 /* UIApplicationTracker.swift */; }; + DA26419C2CC0499300CB427B /* PostHogAutocaptureEventTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA26419A2CC0499300CB427B /* PostHogAutocaptureEventTracker.swift */; }; + DA5B85882CD21CBB00686389 /* AutocaptureEventProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA5B85872CD21CBB00686389 /* AutocaptureEventProcessing.swift */; }; + DA979D7B2CD370B700F56BAE /* PostHogAutocaptureEventTrackerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA979D7A2CD370B700F56BAE /* PostHogAutocaptureEventTrackerSpec.swift */; }; + DAC699D62CC790D9000D1D6B /* PostHogAutocaptureIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC699D52CC790D9000D1D6B /* PostHogAutocaptureIntegration.swift */; }; + DAC699EC2CCA73E5000D1D6B /* ForwardingPickerViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC699EB2CCA73E5000D1D6B /* ForwardingPickerViewDelegate.swift */; }; + DACF6D5D2CD2F5BC00F14133 /* PostHogAutocaptureIntegrationSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DACF6D5C2CD2F5BC00F14133 /* PostHogAutocaptureIntegrationSpec.swift */; }; DAD5DD0C2CB6DEF30087387B /* PostHogMaskViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAD5DD072CB6DEE70087387B /* PostHogMaskViewModifier.swift */; }; /* End PBXBuildFile section */ @@ -200,6 +206,13 @@ remoteGlobalIDString = 3AC745B4296D6FE60025C109; remoteInfo = PostHog; }; + DA8D37282CBEAC03005EBD27 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DA8D37242CBEAC02005EBD27 /* PostHogExampleAutocapture.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 228DB9F318BC53F1002BA12A; + remoteInfo = PostHogExampleAutocapture; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -376,6 +389,13 @@ 69F518192BAC81FC00F52C14 /* UITextInputTraits+Util.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextInputTraits+Util.swift"; sourceTree = ""; }; 69F518372BB2BA0100F52C14 /* PostHogSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogSwizzler.swift; sourceTree = ""; }; 69F518392BB2BA8300F52C14 /* UIApplicationTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationTracker.swift; sourceTree = ""; }; + DA26419A2CC0499300CB427B /* PostHogAutocaptureEventTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAutocaptureEventTracker.swift; sourceTree = ""; }; + DA5B85872CD21CBB00686389 /* AutocaptureEventProcessing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocaptureEventProcessing.swift; sourceTree = ""; }; + DA8D37242CBEAC02005EBD27 /* PostHogExampleAutocapture.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = PostHogExampleAutocapture.xcodeproj; path = PostHogExampleAutocapture/PostHogExampleAutocapture.xcodeproj; sourceTree = ""; }; + DA979D7A2CD370B700F56BAE /* PostHogAutocaptureEventTrackerSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostHogAutocaptureEventTrackerSpec.swift; sourceTree = ""; }; + DAC699D52CC790D9000D1D6B /* PostHogAutocaptureIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAutocaptureIntegration.swift; sourceTree = ""; }; + DAC699EB2CCA73E5000D1D6B /* ForwardingPickerViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForwardingPickerViewDelegate.swift; sourceTree = ""; }; + DACF6D5C2CD2F5BC00F14133 /* PostHogAutocaptureIntegrationSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAutocaptureIntegrationSpec.swift; sourceTree = ""; }; DAD5DD072CB6DEE70087387B /* PostHogMaskViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogMaskViewModifier.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -503,6 +523,7 @@ 3AC745AB296D6FE60025C109 = { isa = PBXGroup; children = ( + DA8D37242CBEAC02005EBD27 /* PostHogExampleAutocapture.xcodeproj */, 690FF1732AF3CE8A00A0B06B /* PostHogExampleWatchOS.xcodeproj */, 690FF0532AE7DB3700A0B06B /* PostHogExampleWithSPM.xcodeproj */, 690FF02F2AE7C5BA00A0B06B /* PostHogExampleWithPods.xcodeproj */, @@ -536,6 +557,7 @@ 3AC745B7296D6FE60025C109 /* PostHog */ = { isa = PBXGroup; children = ( + DA26419B2CC0499300CB427B /* Autocapture */, 69EE82B82BA9C4DA00EB9542 /* Replay */, 69BA38E62B893F2200AA69D6 /* Resources */, 69779BED2AE6B29E00D7A48E /* Models */, @@ -579,6 +601,8 @@ 690FF0E22AEFD12900A0B06B /* PostHogConfigTest.swift */, 690FF0E82AEFD3BD00A0B06B /* PostHogQueueTest.swift */, 690FF0F42AF0F06100A0B06B /* PostHogSDKTest.swift */, + DACF6D5C2CD2F5BC00F14133 /* PostHogAutocaptureIntegrationSpec.swift */, + DA979D7A2CD370B700F56BAE /* PostHogAutocaptureEventTrackerSpec.swift */, 699C5FEE2C20242A007DB818 /* UUIDTest.swift */, 693E977C2C6257F9004B1030 /* ExampleSanitizer.swift */, 69ED1AB52C90711D00FE7A91 /* PostHogSDKPersonProfilesTest.swift */, @@ -733,6 +757,25 @@ path = PostHogExampleStoryboard; sourceTree = ""; }; + DA26419B2CC0499300CB427B /* Autocapture */ = { + isa = PBXGroup; + children = ( + DA5B85872CD21CBB00686389 /* AutocaptureEventProcessing.swift */, + DAC699EB2CCA73E5000D1D6B /* ForwardingPickerViewDelegate.swift */, + DAC699D52CC790D9000D1D6B /* PostHogAutocaptureIntegration.swift */, + DA26419A2CC0499300CB427B /* PostHogAutocaptureEventTracker.swift */, + ); + path = Autocapture; + sourceTree = ""; + }; + DA8D37252CBEAC02005EBD27 /* Products */ = { + isa = PBXGroup; + children = ( + DA8D37292CBEAC03005EBD27 /* PostHogExampleAutocapture.app */, + ); + name = Products; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -939,6 +982,10 @@ productRefGroup = 3AC745B6296D6FE60025C109 /* Products */; projectDirPath = ""; projectReferences = ( + { + ProductGroup = DA8D37252CBEAC02005EBD27 /* Products */; + ProjectRef = DA8D37242CBEAC02005EBD27 /* PostHogExampleAutocapture.xcodeproj */; + }, { ProductGroup = 690FF1742AF3CE8A00A0B06B /* Products */; ProjectRef = 690FF1732AF3CE8A00A0B06B /* PostHogExampleWatchOS.xcodeproj */; @@ -994,6 +1041,13 @@ remoteRef = 690FF17A2AF3CE8B00A0B06B /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + DA8D37292CBEAC03005EBD27 /* PostHogExampleAutocapture.app */ = { + isa = PBXReferenceProxy; + fileType = wrapper.application; + path = PostHogExampleAutocapture.app; + remoteRef = DA8D37282CBEAC03005EBD27 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; /* End PBXReferenceProxy section */ /* Begin PBXResourcesBuildPhase section */ @@ -1093,10 +1147,14 @@ 69779BEC2AE68E6900D7A48E /* UIViewController.swift in Sources */, 3A0F108929C9BD76002C0084 /* Errors.swift in Sources */, 3AE3FB37299162EA00AFFC18 /* PostHogApi.swift in Sources */, + DAC699D62CC790D9000D1D6B /* PostHogAutocaptureIntegration.swift in Sources */, 6926DA8E2ADD2876005760D2 /* PostHogContext.swift in Sources */, 690FF0AF2AEB9C1400A0B06B /* DateUtils.swift in Sources */, 69F518162BAC7F9200F52C14 /* UIView+Util.swift in Sources */, 69261D192AD9673500232EC7 /* PostHogBatchUploadInfo.swift in Sources */, + DAC699EC2CCA73E5000D1D6B /* ForwardingPickerViewDelegate.swift in Sources */, + DA26419C2CC0499300CB427B /* PostHogAutocaptureEventTracker.swift in Sources */, + DA5B85882CD21CBB00686389 /* AutocaptureEventProcessing.swift in Sources */, 69ED1A5C2C7F15F300FE7A91 /* PostHogSessionManager.swift in Sources */, 69F23A742BB3088E001194F6 /* URLSessionSwizzler.swift in Sources */, 69F518142BAC7F4300F52C14 /* Date+Util.swift in Sources */, @@ -1136,8 +1194,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DA979D7B2CD370B700F56BAE /* PostHogAutocaptureEventTrackerSpec.swift in Sources */, 690FF0F52AF0F06100A0B06B /* PostHogSDKTest.swift in Sources */, 690FF0E12AEFC59100A0B06B /* PostHogFileBackedQueueTest.swift in Sources */, + DACF6D5D2CD2F5BC00F14133 /* PostHogAutocaptureIntegrationSpec.swift in Sources */, 3A62647529CB0168007E8C07 /* TestPostHog.swift in Sources */, 699C5FEF2C20242A007DB818 /* UUIDTest.swift in Sources */, 69ED1AB62C90711D00FE7A91 /* PostHogSDKPersonProfilesTest.swift in Sources */, diff --git a/PostHog/Autocapture/AutocaptureEventProcessing.swift b/PostHog/Autocapture/AutocaptureEventProcessing.swift new file mode 100644 index 000000000..121db123d --- /dev/null +++ b/PostHog/Autocapture/AutocaptureEventProcessing.swift @@ -0,0 +1,14 @@ +// +// AutocaptureEventProcessing.swift +// PostHog +// +// Created by Yiannis Josephides on 30/10/2024. +// + +#if os(iOS) || targetEnvironment(macCatalyst) + import Foundation + + protocol AutocaptureEventProcessing: AnyObject { + func process(source: PostHogAutocaptureEventTracker.EventSource, event: PostHogAutocaptureEventTracker.EventData) + } +#endif diff --git a/PostHog/Autocapture/ForwardingPickerViewDelegate.swift b/PostHog/Autocapture/ForwardingPickerViewDelegate.swift new file mode 100644 index 000000000..1511b7f8a --- /dev/null +++ b/PostHog/Autocapture/ForwardingPickerViewDelegate.swift @@ -0,0 +1,960 @@ +// swiftlint:disable cyclomatic_complexity + +// +// ForwardingPickerViewDelegate.swift +// PostHog +// +// Created by Yiannis Josephides on 24/10/2024. +// + +#if os(iOS) || targetEnvironment(macCatalyst) + import Foundation + import UIKit + + enum ForwardingDelegateSelector { + static func selectDelegate(for actualDelegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) -> UIPickerViewDelegate { + // Checking if the actual delegate implements specific methods + let titleForRow = actualDelegate?.responds(to: #selector(UIPickerViewDelegate.pickerView(_:titleForRow:forComponent:))) ?? false + let attributedTitleForRow = actualDelegate?.responds(to: #selector(UIPickerViewDelegate.pickerView(_:attributedTitleForRow:forComponent:))) ?? false + let viewForRow = actualDelegate?.responds(to: #selector(UIPickerViewDelegate.pickerView(_:viewForRow:forComponent:reusing:))) ?? false + let rowHeightForComponent = actualDelegate?.responds(to: #selector(UIPickerViewDelegate.pickerView(_:rowHeightForComponent:))) ?? false + let widthForComponent = actualDelegate?.responds(to: #selector(UIPickerViewDelegate.pickerView(_:widthForComponent:))) ?? false + + // Selecting the appropriate forwarding delegate based on implemented methods + // + // UIPickerViewDelegate includes several `optional` methods that return values. + // + // To ensure that the behavior of the host app is preserved, we must select a forwarding delegate that accurately + // reflects which methods are implemented by the original delegate. This ensures the forwarding delegate + // responds correctly to `responds(to: #selector)` checks and avoids dependeing on abstract default values + + switch (titleForRow, attributedTitleForRow, viewForRow, rowHeightForComponent, widthForComponent) { + case (false, false, false, false, false): + return ForwardingPickerViewDelegate1(delegate: actualDelegate, onValueChanged: onValueChanged) + case (true, false, false, false, false): + return ForwardingPickerViewDelegate2(delegate: actualDelegate, onValueChanged: onValueChanged) + case (false, true, false, false, false): + return ForwardingPickerViewDelegate3(delegate: actualDelegate, onValueChanged: onValueChanged) + case (false, false, true, false, false): + return ForwardingPickerViewDelegate4(delegate: actualDelegate, onValueChanged: onValueChanged) + case (false, false, false, true, false): + return ForwardingPickerViewDelegate5(delegate: actualDelegate, onValueChanged: onValueChanged) + case (false, false, false, false, true): + return ForwardingPickerViewDelegate6(delegate: actualDelegate, onValueChanged: onValueChanged) + case (true, true, false, false, false): + return ForwardingPickerViewDelegate7(delegate: actualDelegate, onValueChanged: onValueChanged) + case (true, false, true, false, false): + return ForwardingPickerViewDelegate8(delegate: actualDelegate, onValueChanged: onValueChanged) + case (true, false, false, true, false): + return ForwardingPickerViewDelegate9(delegate: actualDelegate, onValueChanged: onValueChanged) + case (true, false, false, false, true): + return ForwardingPickerViewDelegate10(delegate: actualDelegate, onValueChanged: onValueChanged) + case (false, true, true, false, false): + return ForwardingPickerViewDelegate11(delegate: actualDelegate, onValueChanged: onValueChanged) + case (false, true, false, true, false): + return ForwardingPickerViewDelegate12(delegate: actualDelegate, onValueChanged: onValueChanged) + case (false, true, false, false, true): + return ForwardingPickerViewDelegate13(delegate: actualDelegate, onValueChanged: onValueChanged) + case (false, false, true, true, false): + return ForwardingPickerViewDelegate14(delegate: actualDelegate, onValueChanged: onValueChanged) + case (false, false, true, false, true): + return ForwardingPickerViewDelegate15(delegate: actualDelegate, onValueChanged: onValueChanged) + case (false, false, false, true, true): + return ForwardingPickerViewDelegate16(delegate: actualDelegate, onValueChanged: onValueChanged) + case (true, true, true, false, false): + return ForwardingPickerViewDelegate17(delegate: actualDelegate, onValueChanged: onValueChanged) + case (true, true, false, true, false): + return ForwardingPickerViewDelegate18(delegate: actualDelegate, onValueChanged: onValueChanged) + case (true, true, false, false, true): + return ForwardingPickerViewDelegate19(delegate: actualDelegate, onValueChanged: onValueChanged) + case (true, false, true, true, false): + return ForwardingPickerViewDelegate20(delegate: actualDelegate, onValueChanged: onValueChanged) + case (true, false, true, false, true): + return ForwardingPickerViewDelegate21(delegate: actualDelegate, onValueChanged: onValueChanged) + case (true, false, false, true, true): + return ForwardingPickerViewDelegate22(delegate: actualDelegate, onValueChanged: onValueChanged) + case (false, true, true, true, false): + return ForwardingPickerViewDelegate23(delegate: actualDelegate, onValueChanged: onValueChanged) + case (false, true, true, false, true): + return ForwardingPickerViewDelegate24(delegate: actualDelegate, onValueChanged: onValueChanged) + case (false, true, false, true, true): + return ForwardingPickerViewDelegate25(delegate: actualDelegate, onValueChanged: onValueChanged) + case (false, false, true, true, true): + return ForwardingPickerViewDelegate26(delegate: actualDelegate, onValueChanged: onValueChanged) + case (true, true, true, true, false): + return ForwardingPickerViewDelegate27(delegate: actualDelegate, onValueChanged: onValueChanged) + case (true, true, true, false, true): + return ForwardingPickerViewDelegate28(delegate: actualDelegate, onValueChanged: onValueChanged) + case (true, true, false, true, true): + return ForwardingPickerViewDelegate29(delegate: actualDelegate, onValueChanged: onValueChanged) + case (true, false, true, true, true): + return ForwardingPickerViewDelegate30(delegate: actualDelegate, onValueChanged: onValueChanged) + case (false, true, true, true, true): + return ForwardingPickerViewDelegate31(delegate: actualDelegate, onValueChanged: onValueChanged) + case (true, true, true, true, true): + return ForwardingPickerViewDelegate32(delegate: actualDelegate, onValueChanged: onValueChanged) + } + } + } + + private var phForwardingDelegateKey: UInt8 = 0 + extension UIPickerViewDelegate { + var phForwardingDelegate: UIPickerViewDelegate { + get { + objc_getAssociatedObject(self, &phForwardingDelegateKey) as! UIPickerViewDelegate + } + + set { + objc_setAssociatedObject( + self, + &phForwardingDelegateKey, + newValue as UIPickerViewDelegate?, + .OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) + } + } + } + + // MARK: - DELEGATE VARIANTS + + /// Combination 1: `didSelectRow` + private class ForwardingPickerViewDelegate1: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + } + + /// Combination 2: `didSelectRow`, `titleForRow` + private class ForwardingPickerViewDelegate2: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + actualDelegate?.pickerView?(pickerView, titleForRow: row, forComponent: component) + } + } + + /// Combination 3: `didSelectRow`, `attributedTitleForRow` + private class ForwardingPickerViewDelegate3: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + // Call the value changed callback + valueChangedCallback?() + // Forward the call to the actual delegate + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { + actualDelegate?.pickerView?(pickerView, attributedTitleForRow: row, forComponent: component) + } + } + + /// Combination 4: `didSelectRow`, `viewForRow` + private class ForwardingPickerViewDelegate4: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { + actualDelegate?.pickerView?(pickerView, viewForRow: row, forComponent: component, reusing: view) ?? UIView() + } + } + + /// Combination 5: `didSelectRow`, `rowHeightForComponent` + private class ForwardingPickerViewDelegate5: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, rowHeightForComponent: component) ?? .zero + } + } + + /// Combination 6: `didSelectRow`, `widthForComponent` + private class ForwardingPickerViewDelegate6: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, widthForComponent: component) ?? .zero + } + } + + /// Combination 7: `didSelectRow`, `titleForRow`, `attributedTitleForRow` + private class ForwardingPickerViewDelegate7: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + actualDelegate?.pickerView?(pickerView, titleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { + actualDelegate?.pickerView?(pickerView, attributedTitleForRow: row, forComponent: component) + } + } + + /// Combination 8: `didSelectRow`, `titleForRow`, `viewForRow` + private class ForwardingPickerViewDelegate8: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + actualDelegate?.pickerView?(pickerView, titleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { + actualDelegate?.pickerView?(pickerView, viewForRow: row, forComponent: component, reusing: view) ?? UIView() + } + + func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, rowHeightForComponent: component) ?? .zero + } + + func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, widthForComponent: component) ?? .zero + } + } + + /// Combination 9: `didSelectRow`, `titleForRow`, `rowHeightForComponent` + private class ForwardingPickerViewDelegate9: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + actualDelegate?.pickerView?(pickerView, titleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, rowHeightForComponent: component) ?? .zero + } + } + + /// Combination 10: `didSelectRow`, `titleForRow`, `widthForComponent` + private class ForwardingPickerViewDelegate10: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + actualDelegate?.pickerView?(pickerView, titleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, widthForComponent: component) ?? .zero + } + } + + /// Combination 11: `didSelectRow`, `attributedTitleForRow`, `viewForRow` + private class ForwardingPickerViewDelegate11: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { + actualDelegate?.pickerView?(pickerView, attributedTitleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { + actualDelegate?.pickerView?(pickerView, viewForRow: row, forComponent: component, reusing: view) ?? UIView() + } + } + + /// Combination 12: `didSelectRow`, `attributedTitleForRow`, `rowHeightForComponent` + private class ForwardingPickerViewDelegate12: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { + actualDelegate?.pickerView?(pickerView, attributedTitleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, rowHeightForComponent: component) ?? .zero + } + } + + /// Combination 13: `didSelectRow`, `attributedTitleForRow`, `widthForComponent` + private class ForwardingPickerViewDelegate13: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { + actualDelegate?.pickerView?(pickerView, attributedTitleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, widthForComponent: component) ?? .zero + } + } + + /// Combination 14: `didSelectRow`, `viewForRow`, `rowHeightForComponent` + private class ForwardingPickerViewDelegate14: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { + actualDelegate?.pickerView?(pickerView, viewForRow: row, forComponent: component, reusing: view) ?? UIView() + } + + func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, rowHeightForComponent: component) ?? .zero + } + } + + /// Combination 15: `didSelectRow`, `viewForRow`, `widthForComponent` + private class ForwardingPickerViewDelegate15: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { + actualDelegate?.pickerView?(pickerView, viewForRow: row, forComponent: component, reusing: view) ?? UIView() + } + + func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, widthForComponent: component) ?? .zero + } + } + + /// Combination 16: `didSelectRow`, `rowHeightForComponent`, `widthForComponent` + private class ForwardingPickerViewDelegate16: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, rowHeightForComponent: component) ?? .zero + } + + func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, widthForComponent: component) ?? .zero + } + } + + /// Combination 17: `didSelectRow`, `titleForRow`, `attributedTitleForRow`, `viewForRow` + private class ForwardingPickerViewDelegate17: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + actualDelegate?.pickerView?(pickerView, titleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { + actualDelegate?.pickerView?(pickerView, attributedTitleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { + actualDelegate?.pickerView?(pickerView, viewForRow: row, forComponent: component, reusing: view) ?? UIView() + } + } + + /// Combination 18: `didSelectRow`, `titleForRow`, `attributedTitleForRow`, `rowHeightForComponent` + private class ForwardingPickerViewDelegate18: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + actualDelegate?.pickerView?(pickerView, titleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { + actualDelegate?.pickerView?(pickerView, attributedTitleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, rowHeightForComponent: component) ?? .zero + } + } + + /// Combination 19: `didSelectRow`, `titleForRow`, `attributedTitleForRow`, `widthForComponent` + private class ForwardingPickerViewDelegate19: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + actualDelegate?.pickerView?(pickerView, titleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { + actualDelegate?.pickerView?(pickerView, attributedTitleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, widthForComponent: component) ?? .zero + } + } + + /// Combination 20: `didSelectRow`, `titleForRow`, `viewForRow`, `rowHeightForComponent` + private class ForwardingPickerViewDelegate20: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + actualDelegate?.pickerView?(pickerView, titleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { + actualDelegate?.pickerView?(pickerView, viewForRow: row, forComponent: component, reusing: view) ?? UIView() + } + + func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, rowHeightForComponent: component) ?? .zero + } + } + + /// Combination 21: `didSelectRow`, `titleForRow`, `viewForRow`, `widthForComponent` + private class ForwardingPickerViewDelegate21: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + actualDelegate?.pickerView?(pickerView, titleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { + actualDelegate?.pickerView?(pickerView, viewForRow: row, forComponent: component, reusing: view) ?? UIView() + } + + func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, widthForComponent: component) ?? .zero + } + } + + /// Combination 22: `didSelectRow`, `titleForRow`, `rowHeightForComponent`, `widthForComponent` + private class ForwardingPickerViewDelegate22: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + actualDelegate?.pickerView?(pickerView, titleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, rowHeightForComponent: component) ?? .zero + } + + func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, widthForComponent: component) ?? .zero + } + } + + /// Combination 23: `didSelectRow`, `attributedTitleForRow`, `viewForRow`, `rowHeightForComponent` + private class ForwardingPickerViewDelegate23: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { + actualDelegate?.pickerView?(pickerView, attributedTitleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { + actualDelegate?.pickerView?(pickerView, viewForRow: row, forComponent: component, reusing: view) ?? UIView() + } + + func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, rowHeightForComponent: component) ?? .zero + } + } + + /// Combination 24: `didSelectRow`,`attributedTitleForRow`, `viewForRow`, `widthForComponent` + private class ForwardingPickerViewDelegate24: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { + actualDelegate?.pickerView?(pickerView, attributedTitleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { + actualDelegate?.pickerView?(pickerView, viewForRow: row, forComponent: component, reusing: view) ?? UIView() + } + + func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, widthForComponent: component) ?? .zero + } + } + + /// Combination 25: `didSelectRow`, `attributedTitleForRow`, `rowHeightForComponent`, `widthForComponent` + private class ForwardingPickerViewDelegate25: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { + actualDelegate?.pickerView?(pickerView, attributedTitleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, rowHeightForComponent: component) ?? .zero + } + + func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, widthForComponent: component) ?? .zero + } + } + + /// Combination 26: `didSelectRow`, `viewForRow`, `rowHeightForComponent`, `widthForComponent` + private class ForwardingPickerViewDelegate26: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { + actualDelegate?.pickerView?(pickerView, viewForRow: row, forComponent: component, reusing: view) ?? UIView() + } + + func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, rowHeightForComponent: component) ?? .zero + } + + func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, widthForComponent: component) ?? .zero + } + } + + /// Combination 27: `didSelectRow`, `titleForRow`, `attributedTitleForRow`, `viewForRow`, `rowHeightForComponent` + private class ForwardingPickerViewDelegate27: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + actualDelegate?.pickerView?(pickerView, titleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { + actualDelegate?.pickerView?(pickerView, attributedTitleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { + actualDelegate?.pickerView?(pickerView, viewForRow: row, forComponent: component, reusing: view) ?? UIView() + } + + func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, rowHeightForComponent: component) ?? .zero + } + } + + /// Combination 28: `didSelectRow`, `titleForRow`, `attributedTitleForRow`, `viewForRow`, `widthForComponent` + private class ForwardingPickerViewDelegate28: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + actualDelegate?.pickerView?(pickerView, titleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { + actualDelegate?.pickerView?(pickerView, attributedTitleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { + actualDelegate?.pickerView?(pickerView, viewForRow: row, forComponent: component, reusing: view) ?? UIView() + } + + func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, widthForComponent: component) ?? .zero + } + } + + /// Combination 29: `didSelectRow`, `titleForRow`, `attributedTitleForRow`, `rowHeightForComponent`, `widthForComponent` + private class ForwardingPickerViewDelegate29: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + actualDelegate?.pickerView?(pickerView, titleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { + actualDelegate?.pickerView?(pickerView, attributedTitleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, rowHeightForComponent: component) ?? .zero + } + + func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, widthForComponent: component) ?? .zero + } + } + + /// Combination 30: `didSelectRow`, `titleForRow`, `viewForRow`, `rowHeightForComponent`, `widthForComponent` + private class ForwardingPickerViewDelegate30: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + actualDelegate?.pickerView?(pickerView, titleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { + actualDelegate?.pickerView?(pickerView, viewForRow: row, forComponent: component, reusing: view) ?? UIView() + } + + func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, rowHeightForComponent: component) ?? .zero + } + + func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, widthForComponent: component) ?? .zero + } + } + + /// Combination 31: `didSelectRow`, `attributedTitleForRow`, `viewForRow`, `rowHeightForComponent`, `widthForComponent` + private class ForwardingPickerViewDelegate31: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { + actualDelegate?.pickerView?(pickerView, attributedTitleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { + actualDelegate?.pickerView?(pickerView, viewForRow: row, forComponent: component, reusing: view) ?? UIView() + } + + func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, rowHeightForComponent: component) ?? .zero + } + + func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, widthForComponent: component) ?? .zero + } + } + + /// Combination 32: `didSelectRow`, `titleForRow`, `attributedTitleForRow`, `viewForRow`, `rowHeightForComponent`, `widthForComponent` + private class ForwardingPickerViewDelegate32: NSObject, UIPickerViewDelegate { + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + actualDelegate?.pickerView?(pickerView, titleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { + actualDelegate?.pickerView?(pickerView, attributedTitleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { + actualDelegate?.pickerView?(pickerView, viewForRow: row, forComponent: component, reusing: view) ?? UIView() + } + + func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, rowHeightForComponent: component) ?? .zero + } + + func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, widthForComponent: component) ?? .zero + } + } + +#endif + +// swiftlint:enable cyclomatic_complexity diff --git a/PostHog/Autocapture/PostHogAutocaptureEventTracker.swift b/PostHog/Autocapture/PostHogAutocaptureEventTracker.swift new file mode 100644 index 000000000..fc70a5cac --- /dev/null +++ b/PostHog/Autocapture/PostHogAutocaptureEventTracker.swift @@ -0,0 +1,549 @@ +// +// PostHogAutocaptureEventTracker.swift +// PostHog +// +// Created by Yiannis Josephides on 14/10/2024. +// + +#if os(iOS) || targetEnvironment(macCatalyst) + import UIKit + + class PostHogAutocaptureEventTracker { + struct EventData { + let touchCoordinates: CGPoint? + let value: String? + let screenName: String? + let viewHierarchy: [ViewNode] + let targetClass: String + let accessibilityLabel: String? + let accessibilityIdentifier: String? + // values >0 means that this event will be debounced for `debounceInterval` + let debounceInterval: TimeInterval + } + + struct ViewNode: CustomStringConvertible { + let text: String + let targetClass: String + let index: Int + let subviewCount: Int + + // used when building $elements_chain + // Note: For some reason text will not be processed if not present in elements_chain string. + // Couldn't pinpoint to exact place in `posthog` repo where we check for this though + var description: String { + "\(targetClass)\(text.isEmpty ? "" : ":text=\"\(text)\"")" + } + } + + enum EventSource { + case notification(name: String) + case actionMethod(description: String) + case gestureRecognizer(description: String) + } + + static var eventProcessor: (any AutocaptureEventProcessing)? { + willSet { + if newValue != nil { + swizzle() + } else { + unswizzle() + } + } + } + + private static var hasSwizzled: Bool = false + private static func swizzle() { + guard !hasSwizzled else { return } + hasSwizzled = true + swizzleMethods() + registerNotifications() + } + + private static func unswizzle() { + guard hasSwizzled else { return } + hasSwizzled = false + swizzleMethods() // swizzling again will excahnge implementations back to original + unregisterNotifications() + } + + private static func swizzleMethods() { + PostHog.swizzle( + forClass: UIApplication.self, + original: #selector(UIApplication.sendAction), + new: #selector(UIApplication.ph_swizzled_uiapplication_sendAction) + ) + + PostHog.swizzle( + forClass: UIGestureRecognizer.self, + original: #selector(setter: UIGestureRecognizer.state), + new: #selector(UIGestureRecognizer.ph_swizzled_uigesturerecognizer_state_Setter) + ) + + PostHog.swizzle( + forClass: UIScrollView.self, + original: #selector(setter: UIScrollView.contentOffset), + new: #selector(UIScrollView.ph_swizzled_setContentOffset_Setter) + ) + + PostHog.swizzle( + forClass: UIPickerView.self, + original: #selector(setter: UIPickerView.delegate), + new: #selector(UIPickerView.ph_swizzled_setDelegate) + ) + } + + private static func registerNotifications() { + NotificationCenter.default.addObserver( + PostHogAutocaptureEventTracker.self, + selector: #selector(didEndEditing), + name: UITextField.textDidEndEditingNotification, + object: nil + ) + NotificationCenter.default.addObserver( + PostHogAutocaptureEventTracker.self, + selector: #selector(didEndEditing), + name: UITextView.textDidEndEditingNotification, + object: nil + ) + } + + private static func unregisterNotifications() { + NotificationCenter.default.removeObserver(PostHogAutocaptureEventTracker.self, name: UITextField.textDidEndEditingNotification, object: nil) + NotificationCenter.default.removeObserver(PostHogAutocaptureEventTracker.self, name: UITextView.textDidEndEditingNotification, object: nil) + } + + // `UITextField` or `UITextView` did end editing notification + @objc static func didEndEditing(_ notification: NSNotification) { + guard let view = notification.object as? UIView, let eventData = view.eventData else { return } + + eventProcessor?.process(source: .notification(name: "change"), event: eventData) + } + } + + extension UIApplication { + @objc func ph_swizzled_uiapplication_sendAction(_ action: Selector, to target: Any?, from sender: Any?, for event: UIEvent?) -> Bool { + defer { + // Currently, the action methods pointing to a SwiftUI target are blocked. + let targetClass = String(describing: object_getClassName(target)) + if targetClass.contains("SwiftUI") { + hedgeLog("Action methods on SwiftUI targets are not yet supported.") + } else if let control = sender as? UIControl, + control.ph_shouldTrack(action, for: target), + let eventData = control.eventData, + let eventDescription = control.event(for: action, to: target)?.description(forControl: control) + { + PostHogAutocaptureEventTracker.eventProcessor?.process(source: .actionMethod(description: eventDescription), event: eventData) + } + } + + // first, call original method + return ph_swizzled_uiapplication_sendAction(action, to: target, from: sender, for: event) + } + } + + extension UIGestureRecognizer { + // swiftlint:disable:next cyclomatic_complexity + @objc func ph_swizzled_uigesturerecognizer_state_Setter(_ state: UIGestureRecognizer.State) { + // first, call original method + ph_swizzled_uigesturerecognizer_state_Setter(state) + + guard state == .ended, let view, shouldTrack(view) else { return } + + // block scroll and zoom gestures for `UIScrollView`. + if let scrollView = view as? UIScrollView { + if self === scrollView.panGestureRecognizer { + return + } + #if !os(tvOS) + if self === scrollView.pinchGestureRecognizer { + return + } + #endif + } + + // block all gestures for `UISwitch` (already captured via `.valueChanged` action) + if String(describing: type(of: view)).starts(with: "UISwitch") { + return + } + // ignore gestures in `UIPickerColumnView` + if String(describing: type(of: view)) == "UIPickerColumnView" { + return + } + + let gestureDescription: String? + switch self { + case is UITapGestureRecognizer: + gestureDescription = EventType.kTouch + case is UISwipeGestureRecognizer: + gestureDescription = EventType.kSwipe + case is UIPanGestureRecognizer: + gestureDescription = EventType.kPan + case is UILongPressGestureRecognizer: + gestureDescription = EventType.kLongPress + #if !os(tvOS) + case is UIPinchGestureRecognizer: + gestureDescription = EventType.kPinch + case is UIRotationGestureRecognizer: + gestureDescription = EventType.kRotation + case is UIScreenEdgePanGestureRecognizer: + gestureDescription = EventType.kPan + #endif + default: + gestureDescription = nil + } + + guard let gestureDescription else { return } + + if let eventData = view.eventData { + PostHogAutocaptureEventTracker.eventProcessor?.process(source: .gestureRecognizer(description: gestureDescription), event: eventData) + } + } + } + + extension UIScrollView { + @objc func ph_swizzled_setContentOffset_Setter(_ contentOffset: CGPoint) { + // first, call original method + ph_swizzled_setContentOffset_Setter(contentOffset) + + // block scrolls on UIPickerTableView. (captured via a forwarding delegate implementation) + if String(describing: type(of: self)) == "UIPickerTableView" { + return + } + + if let eventData { + PostHogAutocaptureEventTracker.eventProcessor?.process(source: .gestureRecognizer(description: EventType.kScroll), event: eventData) + } + } + } + + extension UIPickerView { + @objc func ph_swizzled_setDelegate(_ delegate: (any UIPickerViewDelegate)?) { + // If the delegate implements pickerView(_:didSelectRow:inComponent:), swizzle it + guard let delegate else { + return ph_swizzled_setDelegate(delegate) + } + guard delegate.responds(to: #selector(UIPickerViewDelegate.pickerView(_:didSelectRow:inComponent:))) else { + return ph_swizzled_setDelegate(delegate) + } + + let forwardingDelegate = ForwardingDelegateSelector.selectDelegate(for: delegate) { [weak self] in + if let data = self?.eventData { + PostHogAutocaptureEventTracker.eventProcessor?.process(source: .gestureRecognizer(description: EventType.kValueChange), event: data) + } + } + + // Need to keep a strong reference to keep this forwarding delegate instance alive + delegate.phForwardingDelegate = forwardingDelegate + + ph_swizzled_setDelegate(forwardingDelegate) + } + } + + extension UIView { + var eventData: PostHogAutocaptureEventTracker.EventData? { + guard shouldTrack(self) else { return nil } + return PostHogAutocaptureEventTracker.EventData( + touchCoordinates: nil, + value: ph_autocaptureText + .map(sanitizeText), + screenName: nearestViewController + .flatMap(UIViewController.ph_topViewController) + .flatMap(UIViewController.getViewControllerName), + viewHierarchy: sequence(first: self, next: \.superview) + .enumerated() + .map { $1.viewNode(index: $0) }, + targetClass: descriptiveTypeName, + accessibilityLabel: accessibilityLabel, + accessibilityIdentifier: accessibilityIdentifier, + debounceInterval: ph_autocaptureDebounceInterval + ) + } + } + + extension UIView { + func viewNode(index: Int) -> PostHogAutocaptureEventTracker.ViewNode { + PostHogAutocaptureEventTracker.ViewNode( + text: ph_autocaptureText.map(sanitizeText) ?? "", + targetClass: descriptiveTypeName, + index: index, + subviewCount: subviews.count + ) + } + } + + extension UIControl { + func event(for action: Selector, to target: Any?) -> UIControl.Event? { + var events: [UIControl.Event] = [ + .valueChanged, + .touchDown, + .touchDownRepeat, + .touchDragInside, + .touchDragOutside, + .touchDragEnter, + .touchDragExit, + .touchUpInside, + .touchUpOutside, + .touchCancel, + .editingDidBegin, + .editingChanged, + .editingDidEnd, + .editingDidEndOnExit, + .primaryActionTriggered, + ] + + if #available(iOS 14.0, tvOS 14.0, macCatalyst 14.0, *) { + events.append(.menuActionTriggered) + } + + // latest event for action + return events.first { event in + self.actions(forTarget: target, forControlEvent: event)?.contains(action.description) ?? false + } + } + } + + extension UIControl.Event { + // swiftlint:disable:next cyclomatic_complexity + func description(forControl control: UIControl) -> String? { + if self == .primaryActionTriggered { + if control is UIButton { + return EventType.kTouch // UIButton triggers primaryAction with a touch interaction + } else if control is UISegmentedControl { + return EventType.kValueChange // UISegmentedControl changes its value + } else if control is UITextField { + return EventType.kSubmit // UITextField uses this for submit-like behavior + } else if control is UISwitch { + return EventType.kToggle + } else if control is UIDatePicker { + return EventType.kValueChange + } else if control is UIStepper { + return EventType.kValueChange + } else { + return EventType.kPrimaryAction + } + } + + // General event descriptions + if UIControl.Event.allTouchEvents.contains(self) { + return EventType.kTouch + } else if UIControl.Event.allEditingEvents.contains(self) { + return EventType.kChange + } else if self == .valueChanged { + if control is UISwitch { + // toggle better describes a value chagne in a switch control + return EventType.kToggle + } + return EventType.kValueChange + } else if #available(iOS 14.0, tvOS 14.0, macCatalyst 14.0, *), self == .menuActionTriggered { + return EventType.kMenuAction + } + + return nil + } + } + + extension UIApplication { + static var ph_currentWindow: UIWindow? { + Array(UIApplication.shared.connectedScenes) + .compactMap { $0 as? UIWindowScene } + .flatMap(\.windows) + .first { $0.windowLevel != .statusBar } + } + } + + extension UIViewController { + class func ph_topViewController(base: UIViewController? = UIApplication.ph_currentWindow?.rootViewController) -> UIViewController? { + if let nav = base as? UINavigationController { + return ph_topViewController(base: nav.visibleViewController) + + } else if let tab = base as? UITabBarController, let selected = tab.selectedViewController { + return ph_topViewController(base: selected) + + } else if let presented = base?.presentedViewController { + return ph_topViewController(base: presented) + } + return base + } + } + + extension UIResponder { + var nearestViewController: UIViewController? { + self as? UIViewController ?? next?.nearestViewController + } + } + + extension NSObject { + var descriptiveTypeName: String { + String(describing: type(of: self)) + } + } + + protocol AutoCapturable { + var ph_autocaptureText: String? { get } + var ph_autocaptureEvents: UIControl.Event { get } + var ph_autocaptureDebounceInterval: TimeInterval { get } + func ph_shouldTrack(_ action: Selector, for target: Any?) -> Bool + } + + extension UIView: AutoCapturable { + @objc var ph_autocaptureEvents: UIControl.Event { .touchUpInside } + @objc var ph_autocaptureText: String? { nil } + @objc var ph_autocaptureDebounceInterval: TimeInterval { 0 } + @objc func ph_shouldTrack(_: Selector, for _: Any?) -> Bool { + false // by default views are not tracked. Can be overriden in subclasses + } + } + + extension UIButton { + override var ph_autocaptureText: String? { title(for: .normal) ?? title(for: .selected) } + } + + extension UIControl { + @objc override func ph_shouldTrack(_ action: Selector, for target: Any?) -> Bool { + guard shouldTrack(self) else { return false } + return actions(forTarget: target, forControlEvent: ph_autocaptureEvents)?.contains(action.description) ?? false + } + } + + extension UIScrollView { + override var ph_autocaptureDebounceInterval: TimeInterval { 0.4 } + } + + extension UISegmentedControl { + override var ph_autocaptureEvents: UIControl.Event { .valueChanged } + override var ph_autocaptureText: String? { titleForSegment(at: selectedSegmentIndex) } + } + + extension UIPageControl { + override var ph_autocaptureEvents: UIControl.Event { .valueChanged } + } + + extension UISearchBar { + override var ph_autocaptureEvents: UIControl.Event { .editingDidEnd } + } + + extension UIToolbar { + override var ph_autocaptureEvents: UIControl.Event { + if #available(iOS 14.0, *) { .menuActionTriggered } else { .primaryActionTriggered } + } + } + + extension UITextField { + override var ph_autocaptureText: String? { text ?? attributedText?.string ?? placeholder } + override func ph_shouldTrack(_: Selector, for _: Any?) -> Bool { + // Just making sure that in the future we don't intercept UIControl.Ecent (even though it's not currently emited) + // Trakced via `UITextField.textDidEndEditingNotification` + false + } + } + + extension UITextView { + override var ph_autocaptureText: String? { text ?? attributedText?.string } + override func ph_shouldTrack(_: Selector, for _: Any?) -> Bool { + shouldTrack(self) + } + } + + extension UIStepper { + override var ph_autocaptureEvents: UIControl.Event { .valueChanged } + override var ph_autocaptureText: String? { "\(value)" } + } + + extension UISlider { + override var ph_autocaptureDebounceInterval: TimeInterval { 0.3 } + override var ph_autocaptureEvents: UIControl.Event { .valueChanged } + override var ph_autocaptureText: String? { "\(value)" } + } + + extension UISwitch { + @objc override var ph_autocaptureEvents: UIControl.Event { .valueChanged } + override var ph_autocaptureText: String? { "\(isOn)" } + } + + extension UIPickerView { + override var ph_autocaptureText: String? { + (0 ..< numberOfComponents).reduce("") { result, component in + let selectedRow = selectedRow(inComponent: component) + if let title = delegate?.pickerView?(self, titleForRow: selectedRow, forComponent: component) { + return result.isEmpty ? title : "\(result) \(title)" + } + if let title = delegate?.pickerView?(self, attributedTitleForRow: selectedRow, forComponent: component) { + return result.isEmpty ? title.string : "\(result) \(title.string)" + } + return result + } + } + } + + #if !os(tvOS) + extension UIDatePicker { + override var ph_autocaptureEvents: UIControl.Event { .valueChanged } + } + #endif + + private func shouldTrack(_ view: UIView) -> Bool { + if view.isHidden { return false } + if !view.isUserInteractionEnabled { return false } + if view.isNoCapture() { return false } + + if let textField = view as? UITextField, textField.isSensitiveText() { + return false + } + if let textView = view as? UITextView, textView.isSensitiveText() { + return false + } + + // check view hierarchy up + if let superview = view.superview { + return shouldTrack(superview) + } + + return true + } + + // TODO: Filter out or obfuscsate strings that look like sensitive data + // see: https://github.com/PostHog/posthog-js/blob/0cfffcac9bdf1da3fbb9478c1a51170a325bd57f/src/autocapture-utils.ts#L389 + private func sanitizeText(_ title: String) -> String { + title + .trimmingCharacters(in: .whitespacesAndNewlines) // trim + .replacingOccurrences( // sequence of spaces, returns and line breaks + of: "[ \\r\\n]+", + with: " ", + options: .regularExpression + ) + .replacingOccurrences( // sanitize zero-width unicode characters + of: "[\\u{200B}\\u{200C}\\u{200D}\\u{FEFF}]", + with: "", + options: .regularExpression + ) + .limit(to: 255) + } + + enum EventType { + static let kValueChange = "value_changed" + static let kSubmit = "submit" + static let kToggle = "toggle" + static let kPrimaryAction = "primary_action" + static let kMenuAction = "menu_action" + static let kChange = "change" + + static let kTouch = "touch" + static let kSwipe = "swipe" + static let kPinch = "pinch" + static let kPan = "pan" + static let kScroll = "scroll" + static let kRotation = "rotation" + static let kLongPress = "long_press" + } + + private extension String { + func limit(to length: Int) -> String { + if count > length { + let index = index(startIndex, offsetBy: length) + return String(self[.. 0 { + debounceTimers[eventHash]?.invalidate() // Keep cancelling existing + debounceTimers[eventHash] = Timer.scheduledTimer(withTimeInterval: event.debounceInterval, repeats: false) { [weak self] _ in + self?.handleEventProcessing(source: source, event: event) + self?.debounceTimers.removeValue(forKey: eventHash) // Clean up once fired + } + } else { + handleEventProcessing(source: source, event: event) + } + } + + private static let viewHierarchyDelimiter = ";" + /** + Handles the processing of autocapture events by extracting event details, building properties, and sending them to PostHog. + + - Parameters: + - source: The source of the event (action method, gesture, or notification). Values are already mapped to `$event_type` earlier in the chain + - event: The event data including view hierarchy, screen name, and other metadata. + + This function extracts event details such as the event type, view hierarchy, and touch coordinates. + It creates a structured payload with relevant properties (e.g., tag_name, elements, element_chain) and sends it to the + associated PostHog instance for further processing. + */ + private func handleEventProcessing(source: PostHogAutocaptureEventTracker.EventSource, event: PostHogAutocaptureEventTracker.EventData) { + let eventType: String = switch source { + case let .actionMethod(description): description + case let .gestureRecognizer(description): description + case let .notification(name): name + } + + var properties: [String: Any] = [:] + + if let screenName = event.screenName { + properties["$screen_name"] = screenName + } + + let elements = event.viewHierarchy.map { node -> [String: Any] in + [ + "text": node.text, + "tag_name": node.targetClass, // required + "order": node.index, + "attributes": [ // required + "attr__class": node.targetClass, + ], + ].compactMapValues { $0 } + } + + let elementsChain = event.viewHierarchy + .map(\.description) + .joined(separator: PostHogAutocaptureIntegration.viewHierarchyDelimiter) + + if let coordinates = event.touchCoordinates { + properties["$touch_x"] = coordinates.x + properties["$touch_y"] = coordinates.y + } + + PostHogSDK.shared.autocapture( + eventType: eventType, + elements: elements, + elementsChain: elementsChain, + properties: properties + ) + } + } +#endif diff --git a/PostHog/Models/PostHogEvent.swift b/PostHog/Models/PostHogEvent.swift index 9da1c5ec3..1dafe8629 100644 --- a/PostHog/Models/PostHogEvent.swift +++ b/PostHog/Models/PostHogEvent.swift @@ -76,7 +76,7 @@ public class PostHogEvent { "uuid": uuid.uuidString, ] - if let apiKey = apiKey { + if let apiKey { json["api_key"] = apiKey } diff --git a/PostHog/PostHogConfig.swift b/PostHog/PostHogConfig.swift index d3d0b8e41..f8b973bc9 100644 --- a/PostHog/PostHogConfig.swift +++ b/PostHog/PostHogConfig.swift @@ -24,6 +24,12 @@ import Foundation @objc public var preloadFeatureFlags: Bool = true @objc public var captureApplicationLifecycleEvents: Bool = true @objc public var captureScreenViews: Bool = true + #if os(iOS) || targetEnvironment(macCatalyst) + /// Enable autocapture for iOS + /// Experimental support + /// Default: false + @objc public var captureElementInteractions: Bool = false + #endif @objc public var debug: Bool = false @objc public var optOut: Bool = false @objc public var getAnonymousId: ((UUID) -> UUID) = { uuid in uuid } diff --git a/PostHog/PostHogSDK.swift b/PostHog/PostHogSDK.swift index 59f9fffdd..7c2df7509 100644 --- a/PostHog/PostHogSDK.swift +++ b/PostHog/PostHogSDK.swift @@ -52,6 +52,9 @@ let maxRetryDelay = 30.0 /// Internal, only used for testing var shouldReloadFlagsForTesting = true + #if os(iOS) || targetEnvironment(macCatalyst) + private var autocaptureIntegration: PostHogAutocaptureIntegration? + #endif // nonisolated(unsafe) is introduced in Swift 5.10 #if swift(>=5.10) @objc public nonisolated(unsafe) static let shared: PostHogSDK = { @@ -103,6 +106,11 @@ let maxRetryDelay = 30.0 #if os(iOS) replayIntegration = PostHogReplayIntegration(config) #endif + + #if os(iOS) || targetEnvironment(macCatalyst) + autocaptureIntegration = PostHogAutocaptureIntegration(config) + #endif + #if !os(watchOS) do { reachability = try Reachability() @@ -144,6 +152,12 @@ let maxRetryDelay = 30.0 } #endif + #if os(iOS) || targetEnvironment(macCatalyst) + if config.captureElementInteractions { + autocaptureIntegration?.start() + } + #endif + DispatchQueue.main.async { NotificationCenter.default.post(name: PostHogSDK.didStartNotification, object: nil) } @@ -648,6 +662,42 @@ let maxRetryDelay = 30.0 )) } + func autocapture( + eventType: String, + elements: [[String: Any]], + elementsChain: String, + properties: [String: Any] + ) { + if !isEnabled() { + return + } + + if isOptOutState() { + return + } + + guard let queue else { + return + } + + let props = [ + "$event_type": eventType, + "$elements": elements, + "$elements_chain": elementsChain, + ].merging(sanitizeDicionary(properties) ?? [:]) { prop, _ in prop } + + let distinctId = getDistinctId() + + let properties = buildProperties(distinctId: distinctId, properties: props) + let sanitizedProperties = sanitizeProperties(properties) + + queue.add(PostHogEvent( + event: "$autocapture", + distinctId: distinctId, + properties: sanitizedProperties + )) + } + private func sanitizeProperties(_ properties: [String: Any]) -> [String: Any] { if let sanitizer = config.propertiesSanitizer { return sanitizer.sanitize(properties) @@ -932,6 +982,10 @@ let maxRetryDelay = 30.0 replayIntegration?.stop() replayIntegration = nil #endif + #if os(iOS) || targetEnvironment(macCatalyst) + autocaptureIntegration?.stop() + autocaptureIntegration = nil + #endif queue = nil replayQueue = nil config.storageManager?.reset() @@ -1151,6 +1205,12 @@ let maxRetryDelay = 30.0 return config.sessionReplay && isSessionActive() && (featureFlags?.isSessionReplayFlagActive() ?? false) } #endif + + #if os(iOS) || targetEnvironment(macCatalyst) + @objc public func isAutocaptureActive() -> Bool { + isEnabled() && config.captureElementInteractions + } + #endif } // swiftlint:enable file_length cyclomatic_complexity diff --git a/PostHog/UIViewController.swift b/PostHog/UIViewController.swift index 5de4062ec..0b2b72bbd 100644 --- a/PostHog/UIViewController.swift +++ b/PostHog/UIViewController.swift @@ -61,7 +61,7 @@ let name = UIViewController.getViewControllerName(top) - if let name = name { + if let name { PostHogSDK.shared.screen(name) } } diff --git a/PostHogExampleAutocapture/LICENSE/LICENSE.txt b/PostHogExampleAutocapture/LICENSE/LICENSE.txt new file mode 100755 index 000000000..1f0d0578f --- /dev/null +++ b/PostHogExampleAutocapture/LICENSE/LICENSE.txt @@ -0,0 +1,8 @@ +Copyright © 2021 Apple Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture.xcodeproj/.xcodesamplecode.plist b/PostHogExampleAutocapture/PostHogExampleAutocapture.xcodeproj/.xcodesamplecode.plist new file mode 100644 index 000000000..5dd5da85f --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture.xcodeproj/.xcodesamplecode.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture.xcodeproj/project.pbxproj b/PostHogExampleAutocapture/PostHogExampleAutocapture.xcodeproj/project.pbxproj new file mode 100644 index 000000000..231f1a365 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture.xcodeproj/project.pbxproj @@ -0,0 +1,1070 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 60; + objects = { + +/* Begin PBXBuildFile section */ + 2200541E18BC54E8002A6E8B /* ActivityIndicatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2200541A18BC54E8002A6E8B /* ActivityIndicatorViewController.swift */; }; + 2200541F18BC54E8002A6E8B /* AlertControllerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2200541B18BC54E8002A6E8B /* AlertControllerViewController.swift */; }; + 2200542018BC54E8002A6E8B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2200541C18BC54E8002A6E8B /* AppDelegate.swift */; }; + 2200542718BC54EC002A6E8B /* ButtonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2200542118BC54EC002A6E8B /* ButtonViewController.swift */; }; + 2200542818BC54EC002A6E8B /* CustomSearchBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2200542218BC54EC002A6E8B /* CustomSearchBarViewController.swift */; }; + 2200542918BC54EC002A6E8B /* CustomToolbarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2200542318BC54EC002A6E8B /* CustomToolbarViewController.swift */; }; + 2200542A18BC54EC002A6E8B /* DatePickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2200542418BC54EC002A6E8B /* DatePickerController.swift */; }; + 2200542B18BC54EC002A6E8B /* DefaultSearchBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2200542518BC54EC002A6E8B /* DefaultSearchBarViewController.swift */; }; + 2200542C18BC54EC002A6E8B /* DefaultToolbarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2200542618BC54EC002A6E8B /* DefaultToolbarViewController.swift */; }; + 2200543C18BC54F5002A6E8B /* ImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2200542D18BC54F5002A6E8B /* ImageViewController.swift */; }; + 2200543F18BC54F5002A6E8B /* DefaultPageControlViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2200543018BC54F5002A6E8B /* DefaultPageControlViewController.swift */; }; + 2200544118BC54F5002A6E8B /* ProgressViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2200543218BC54F5002A6E8B /* ProgressViewController.swift */; }; + 2200544218BC54F5002A6E8B /* SegmentedControlViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2200543318BC54F5002A6E8B /* SegmentedControlViewController.swift */; }; + 2200544318BC54F5002A6E8B /* SliderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2200543418BC54F5002A6E8B /* SliderViewController.swift */; }; + 2200544518BC54F5002A6E8B /* StepperViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2200543618BC54F5002A6E8B /* StepperViewController.swift */; }; + 2200544618BC54F5002A6E8B /* SwitchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2200543718BC54F5002A6E8B /* SwitchViewController.swift */; }; + 2200544718BC54F5002A6E8B /* TextFieldViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2200543818BC54F5002A6E8B /* TextFieldViewController.swift */; }; + 2200544818BC54F5002A6E8B /* TextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2200543918BC54F5002A6E8B /* TextViewController.swift */; }; + 2200544918BC54F5002A6E8B /* TintedToolbarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2200543A18BC54F5002A6E8B /* TintedToolbarViewController.swift */; }; + 2200544A18BC54F5002A6E8B /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2200543B18BC54F5002A6E8B /* WebViewController.swift */; }; + 228DBA0818BC53F1002BA12A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 228DBA0718BC53F1002BA12A /* Assets.xcassets */; }; + 3E5C084E1974991E00969DD7 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3E5C08501974991E00969DD7 /* Main.storyboard */; }; + 530A4AB626AF352B00C0C649 /* BaseTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 530A4AB526AF352B00C0C649 /* BaseTableViewController.swift */; }; + 5312D0F222848B0200048DE2 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 5312D0F022848B0200048DE2 /* Credits.rtf */; }; + 531AD242265E8B1200113EC6 /* VisualEffectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531AD241265E8B1100113EC6 /* VisualEffectViewController.swift */; }; + 531AD245265E8B8700113EC6 /* VisualEffectViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 531AD243265E8B8700113EC6 /* VisualEffectViewController.storyboard */; }; + 5340A1B62496CF64004F3666 /* DefaultToolbarViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5340A1B02496CF64004F3666 /* DefaultToolbarViewController.storyboard */; }; + 5340A1B72496CF64004F3666 /* CustomToolbarViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5340A1B22496CF64004F3666 /* CustomToolbarViewController.storyboard */; }; + 5340A1B82496CF64004F3666 /* TintedToolbarViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5340A1B42496CF64004F3666 /* TintedToolbarViewController.storyboard */; }; + 5340A1B92496D670004F3666 /* PickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2200543118BC54F5002A6E8B /* PickerViewController.swift */; }; + 535D32B224970EF10011E153 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535D32B124970EF10011E153 /* SceneDelegate.swift */; }; + 5364C08C249696D7009A9A52 /* OutlineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364C08A249696D7009A9A52 /* OutlineViewController.swift */; }; + 5364C0922496BEFD009A9A52 /* DefaultSearchBarViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5364C08E2496BEFD009A9A52 /* DefaultSearchBarViewController.storyboard */; }; + 5364C0932496BEFD009A9A52 /* CustomSearchBarViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5364C0902496BEFD009A9A52 /* CustomSearchBarViewController.storyboard */; }; + 5364C0992496C2B3009A9A52 /* DefaultPageControlViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5364C0952496C2B3009A9A52 /* DefaultPageControlViewController.storyboard */; }; + 5364C09A2496C2B3009A9A52 /* CustomPageControlViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5364C0972496C2B3009A9A52 /* CustomPageControlViewController.storyboard */; }; + 53654E232298881200B999C7 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 53654E222298881200B999C7 /* WebKit.framework */; }; + 5387556B2660933A0041A1B4 /* PointerInteractionButtonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5387556A2660933A0041A1B4 /* PointerInteractionButtonViewController.swift */; }; + 53875571266095990041A1B4 /* PointerInteractionButtonViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5387556F266095990041A1B4 /* PointerInteractionButtonViewController.storyboard */; }; + 538B36F41F2A8E06002AE100 /* DatePickerController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 538B36F21F2A8D97002AE100 /* DatePickerController.storyboard */; }; + 538B36F71F2A8E8A002AE100 /* TextViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 538B36F51F2A8E66002AE100 /* TextViewController.storyboard */; }; + 539029FF1F2A53AD009775E3 /* AlertControllerViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 539029FD1F2A53AD009775E3 /* AlertControllerViewController.storyboard */; }; + 539C6BAE1F27F4980006C5A9 /* ActivityIndicatorViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 539C6BA81F27F4980006C5A9 /* ActivityIndicatorViewController.storyboard */; }; + 539C6BAF1F27F4980006C5A9 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 539C6BAA1F27F4980006C5A9 /* LaunchScreen.storyboard */; }; + 539C6BB01F27F4980006C5A9 /* WebViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 539C6BAC1F27F4980006C5A9 /* WebViewController.storyboard */; }; + 53A2264B26C1AE7000C0EF3F /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 53A2264926C1A70800C0EF3F /* Localizable.stringsdict */; }; + 53A266B52491ED77008EADBB /* ImagePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53A266AE2491ED77008EADBB /* ImagePickerViewController.swift */; }; + 53A266B62491ED77008EADBB /* CustomPageControlViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53A266AF2491ED77008EADBB /* CustomPageControlViewController.swift */; }; + 53A266B82491ED77008EADBB /* ColorPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53A266B12491ED77008EADBB /* ColorPickerViewController.swift */; }; + 53A266B92491ED77008EADBB /* FontPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53A266B22491ED77008EADBB /* FontPickerViewController.swift */; }; + 53A266C22491ED9E008EADBB /* ImagePickerViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 53A266BA2491ED9E008EADBB /* ImagePickerViewController.storyboard */; }; + 53A266C42491ED9E008EADBB /* ColorPickerViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 53A266BE2491ED9E008EADBB /* ColorPickerViewController.storyboard */; }; + 53A266C52491ED9E008EADBB /* FontPickerViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 53A266C02491ED9E008EADBB /* FontPickerViewController.storyboard */; }; + 53B791DA1F85505400AB2FA6 /* content.html in Resources */ = {isa = PBXBuildFile; fileRef = 53B791D41F854B4700AB2FA6 /* content.html */; }; + 53B791DB1F85505700AB2FA6 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53B791D61F854B4700AB2FA6 /* InfoPlist.strings */; }; + 53B791DC1F85505A00AB2FA6 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53B791D81F854B4800AB2FA6 /* Localizable.strings */; }; + 53CB2C59265FFE9900155325 /* ButtonViewController+Configs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53CB2C58265FFE9900155325 /* ButtonViewController+Configs.swift */; }; + 53CB2C60266003A800155325 /* MenuButtonViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 53CB2C5E266003A800155325 /* MenuButtonViewController.storyboard */; }; + 53CB2C62266003B200155325 /* MenuButtonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53CB2C61266003B200155325 /* MenuButtonViewController.swift */; }; + 53CE5AD81F2A89E500D8A656 /* ButtonViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 53CE5AD61F2A89E500D8A656 /* ButtonViewController.storyboard */; }; + 53CE5ADE1F2A8A3D00D8A656 /* ImageViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 53CE5ADC1F2A8A3D00D8A656 /* ImageViewController.storyboard */; }; + 53CE5AE61F2A8AEF00D8A656 /* PickerViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 53CE5AE41F2A8AEF00D8A656 /* PickerViewController.storyboard */; }; + 53CE5AE91F2A8B1000D8A656 /* ProgressViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 53CE5AE71F2A8B1000D8A656 /* ProgressViewController.storyboard */; }; + 53CE5AEC1F2A8B2F00D8A656 /* SegmentedControlViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 53CE5AEA1F2A8B2F00D8A656 /* SegmentedControlViewController.storyboard */; }; + 53CE5AEF1F2A8B4F00D8A656 /* SliderViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 53CE5AED1F2A8B4F00D8A656 /* SliderViewController.storyboard */; }; + 53CE5AF21F2A8B8300D8A656 /* StackViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 53CE5AF01F2A8B8300D8A656 /* StackViewController.storyboard */; }; + 53CE5AF51F2A8BB000D8A656 /* StepperViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 53CE5AF31F2A8BB000D8A656 /* StepperViewController.storyboard */; }; + 53CE5AF81F2A8BD000D8A656 /* SwitchViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 53CE5AF61F2A8BD000D8A656 /* SwitchViewController.storyboard */; }; + 53CE5AFB1F2A8BEB00D8A656 /* TextFieldViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 53CE5AF91F2A8BEB00D8A656 /* TextFieldViewController.storyboard */; }; + 53D054A526790B6E00CD8B1A /* SymbolViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 53D054A326790B6E00CD8B1A /* SymbolViewController.storyboard */; }; + 53D054A726790B7D00CD8B1A /* SymbolViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D054A626790B7D00CD8B1A /* SymbolViewController.swift */; }; + 53F57011265761D500458712 /* CaseElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F57010265761D500458712 /* CaseElement.swift */; }; + B50F41081B1D284700E5147D /* StackViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50F41071B1D284700E5147D /* StackViewController.swift */; }; + DA8D37342CBEAF61005EBD27 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = DA8D37332CBEAF61005EBD27 /* PostHog */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 2200541A18BC54E8002A6E8B /* ActivityIndicatorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorViewController.swift; sourceTree = ""; }; + 2200541B18BC54E8002A6E8B /* AlertControllerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AlertControllerViewController.swift; sourceTree = ""; }; + 2200541C18BC54E8002A6E8B /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 2200542118BC54EC002A6E8B /* ButtonViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ButtonViewController.swift; sourceTree = ""; }; + 2200542218BC54EC002A6E8B /* CustomSearchBarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CustomSearchBarViewController.swift; sourceTree = ""; }; + 2200542318BC54EC002A6E8B /* CustomToolbarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CustomToolbarViewController.swift; sourceTree = ""; }; + 2200542418BC54EC002A6E8B /* DatePickerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = DatePickerController.swift; sourceTree = ""; }; + 2200542518BC54EC002A6E8B /* DefaultSearchBarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultSearchBarViewController.swift; sourceTree = ""; }; + 2200542618BC54EC002A6E8B /* DefaultToolbarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = DefaultToolbarViewController.swift; sourceTree = ""; }; + 2200542D18BC54F5002A6E8B /* ImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ImageViewController.swift; sourceTree = ""; }; + 2200543018BC54F5002A6E8B /* DefaultPageControlViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = DefaultPageControlViewController.swift; sourceTree = ""; }; + 2200543118BC54F5002A6E8B /* PickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = PickerViewController.swift; sourceTree = ""; }; + 2200543218BC54F5002A6E8B /* ProgressViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ProgressViewController.swift; sourceTree = ""; }; + 2200543318BC54F5002A6E8B /* SegmentedControlViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = SegmentedControlViewController.swift; sourceTree = ""; }; + 2200543418BC54F5002A6E8B /* SliderViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = SliderViewController.swift; sourceTree = ""; }; + 2200543618BC54F5002A6E8B /* StepperViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = StepperViewController.swift; sourceTree = ""; }; + 2200543718BC54F5002A6E8B /* SwitchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = SwitchViewController.swift; sourceTree = ""; }; + 2200543818BC54F5002A6E8B /* TextFieldViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldViewController.swift; sourceTree = ""; }; + 2200543918BC54F5002A6E8B /* TextViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = TextViewController.swift; sourceTree = ""; }; + 2200543A18BC54F5002A6E8B /* TintedToolbarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TintedToolbarViewController.swift; sourceTree = ""; }; + 2200543B18BC54F5002A6E8B /* WebViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = ""; }; + 228DB9F318BC53F1002BA12A /* PostHogExampleAutocapture.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PostHogExampleAutocapture.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 228DBA0718BC53F1002BA12A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 3E5C084F1974991E00969DD7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 530A4AB526AF352B00C0C649 /* BaseTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTableViewController.swift; sourceTree = ""; }; + 5312D0F122848B0200048DE2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = Base; path = Base.lproj/Credits.rtf; sourceTree = ""; }; + 531AD241265E8B1100113EC6 /* VisualEffectViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VisualEffectViewController.swift; sourceTree = ""; }; + 531AD244265E8B8700113EC6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/VisualEffectViewController.storyboard; sourceTree = ""; }; + 5340A1B12496CF64004F3666 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/DefaultToolbarViewController.storyboard; sourceTree = ""; }; + 5340A1B32496CF64004F3666 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/CustomToolbarViewController.storyboard; sourceTree = ""; }; + 5340A1B52496CF64004F3666 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/TintedToolbarViewController.storyboard; sourceTree = ""; }; + 535D32B124970EF10011E153 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 5364C08A249696D7009A9A52 /* OutlineViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutlineViewController.swift; sourceTree = ""; }; + 5364C08F2496BEFD009A9A52 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/DefaultSearchBarViewController.storyboard; sourceTree = ""; }; + 5364C0912496BEFD009A9A52 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/CustomSearchBarViewController.storyboard; sourceTree = ""; }; + 5364C0962496C2B3009A9A52 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/DefaultPageControlViewController.storyboard; sourceTree = ""; }; + 5364C0982496C2B3009A9A52 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/CustomPageControlViewController.storyboard; sourceTree = ""; }; + 53654E222298881200B999C7 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; + 5387556A2660933A0041A1B4 /* PointerInteractionButtonViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PointerInteractionButtonViewController.swift; sourceTree = ""; }; + 53875570266095990041A1B4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/PointerInteractionButtonViewController.storyboard; sourceTree = ""; }; + 538B36F31F2A8D97002AE100 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/DatePickerController.storyboard; sourceTree = ""; }; + 538B36F61F2A8E66002AE100 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/TextViewController.storyboard; sourceTree = ""; }; + 539029FE1F2A53AD009775E3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/AlertControllerViewController.storyboard; sourceTree = ""; }; + 539C6BA91F27F4980006C5A9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/ActivityIndicatorViewController.storyboard; sourceTree = ""; }; + 539C6BAB1F27F4980006C5A9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 539C6BAD1F27F4980006C5A9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/WebViewController.storyboard; sourceTree = ""; }; + 53A2264A26C1A70800C0EF3F /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = Base; path = UIKitCatalog/Base.lproj/Localizable.stringsdict; sourceTree = ""; }; + 53A266AE2491ED77008EADBB /* ImagePickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePickerViewController.swift; sourceTree = ""; }; + 53A266AF2491ED77008EADBB /* CustomPageControlViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomPageControlViewController.swift; sourceTree = ""; }; + 53A266B12491ED77008EADBB /* ColorPickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorPickerViewController.swift; sourceTree = ""; }; + 53A266B22491ED77008EADBB /* FontPickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FontPickerViewController.swift; sourceTree = ""; }; + 53A266BB2491ED9E008EADBB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/ImagePickerViewController.storyboard; sourceTree = ""; }; + 53A266BF2491ED9E008EADBB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/ColorPickerViewController.storyboard; sourceTree = ""; }; + 53A266C12491ED9E008EADBB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/FontPickerViewController.storyboard; sourceTree = ""; }; + 53B791D51F854B4700AB2FA6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = Base; path = Base.lproj/content.html; sourceTree = ""; }; + 53B791D71F854B4700AB2FA6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/InfoPlist.strings; sourceTree = ""; }; + 53B791D91F854B4800AB2FA6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Localizable.strings; sourceTree = ""; }; + 53CB2C58265FFE9900155325 /* ButtonViewController+Configs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ButtonViewController+Configs.swift"; sourceTree = ""; }; + 53CB2C5F266003A800155325 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MenuButtonViewController.storyboard; sourceTree = ""; }; + 53CB2C61266003B200155325 /* MenuButtonViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuButtonViewController.swift; sourceTree = ""; }; + 53CE5AD71F2A89E500D8A656 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/ButtonViewController.storyboard; sourceTree = ""; }; + 53CE5ADD1F2A8A3D00D8A656 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/ImageViewController.storyboard; sourceTree = ""; }; + 53CE5AE51F2A8AEF00D8A656 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/PickerViewController.storyboard; sourceTree = ""; }; + 53CE5AE81F2A8B1000D8A656 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/ProgressViewController.storyboard; sourceTree = ""; }; + 53CE5AEB1F2A8B2F00D8A656 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/SegmentedControlViewController.storyboard; sourceTree = ""; }; + 53CE5AEE1F2A8B4F00D8A656 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/SliderViewController.storyboard; sourceTree = ""; }; + 53CE5AF11F2A8B8300D8A656 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/StackViewController.storyboard; sourceTree = ""; }; + 53CE5AF41F2A8BB000D8A656 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/StepperViewController.storyboard; sourceTree = ""; }; + 53CE5AF71F2A8BD000D8A656 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/SwitchViewController.storyboard; sourceTree = ""; }; + 53CE5AFA1F2A8BEB00D8A656 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/TextFieldViewController.storyboard; sourceTree = ""; }; + 53D054A426790B6E00CD8B1A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/SymbolViewController.storyboard; sourceTree = ""; }; + 53D054A626790B7D00CD8B1A /* SymbolViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SymbolViewController.swift; sourceTree = ""; }; + 53F57010265761D500458712 /* CaseElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaseElement.swift; sourceTree = ""; }; + 64D8CD77C30F3CED2D55B8F2 /* LICENSE.txt */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = LICENSE.txt; sourceTree = ""; }; + B50F41071B1D284700E5147D /* StackViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackViewController.swift; sourceTree = ""; }; + DA8D372E2CBEAEDB005EBD27 /* PostHog.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PostHog.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 228DB9F018BC53F1002BA12A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 53654E232298881200B999C7 /* WebKit.framework in Frameworks */, + DA8D37342CBEAF61005EBD27 /* PostHog in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 228DB9EA18BC53F1002BA12A = { + isa = PBXGroup; + children = ( + 228DB9F518BC53F1002BA12A /* PostHogExampleAutocapture */, + 228DB9F418BC53F1002BA12A /* Products */, + 53654E212298881100B999C7 /* Frameworks */, + D9361AEE59ED9397893793F6 /* LICENSE */, + ); + sourceTree = ""; + }; + 228DB9F418BC53F1002BA12A /* Products */ = { + isa = PBXGroup; + children = ( + 228DB9F318BC53F1002BA12A /* PostHogExampleAutocapture.app */, + ); + name = Products; + sourceTree = ""; + }; + 228DB9F518BC53F1002BA12A /* PostHogExampleAutocapture */ = { + isa = PBXGroup; + children = ( + 3E2459D41931CCB5002D3369 /* Application */, + 3E1DA7601931CC99000114A9 /* Controls */, + 539383DE2492800100A489A9 /* Views */, + 539383DF2492801000A489A9 /* Pickers */, + 228DBA0718BC53F1002BA12A /* Assets.xcassets */, + 228DB9F618BC53F1002BA12A /* Supporting Files */, + ); + path = PostHogExampleAutocapture; + sourceTree = ""; + }; + 228DB9F618BC53F1002BA12A /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 3E5C08501974991E00969DD7 /* Main.storyboard */, + 539C6BAA1F27F4980006C5A9 /* LaunchScreen.storyboard */, + 53B791D41F854B4700AB2FA6 /* content.html */, + 53B791D61F854B4700AB2FA6 /* InfoPlist.strings */, + 53B791D81F854B4800AB2FA6 /* Localizable.strings */, + 53A2264926C1A70800C0EF3F /* Localizable.stringsdict */, + 5312D0F022848B0200048DE2 /* Credits.rtf */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 22B7BB9718BC601D006C4AD5 /* Search */ = { + isa = PBXGroup; + children = ( + 2200542518BC54EC002A6E8B /* DefaultSearchBarViewController.swift */, + 2200542218BC54EC002A6E8B /* CustomSearchBarViewController.swift */, + 5364C0902496BEFD009A9A52 /* CustomSearchBarViewController.storyboard */, + 5364C08E2496BEFD009A9A52 /* DefaultSearchBarViewController.storyboard */, + ); + name = Search; + sourceTree = ""; + }; + 22B7BB9818BC6024006C4AD5 /* Toolbar */ = { + isa = PBXGroup; + children = ( + 2200542618BC54EC002A6E8B /* DefaultToolbarViewController.swift */, + 2200543A18BC54F5002A6E8B /* TintedToolbarViewController.swift */, + 2200542318BC54EC002A6E8B /* CustomToolbarViewController.swift */, + 5340A1B22496CF64004F3666 /* CustomToolbarViewController.storyboard */, + 5340A1B02496CF64004F3666 /* DefaultToolbarViewController.storyboard */, + 5340A1B42496CF64004F3666 /* TintedToolbarViewController.storyboard */, + ); + name = Toolbar; + sourceTree = ""; + }; + 3E1DA7601931CC99000114A9 /* Controls */ = { + isa = PBXGroup; + children = ( + 53D34692265D86FF00D0B553 /* Button */, + 53CB2C5A2660038E00155325 /* Menu Button */, + 53875569266093290041A1B4 /* Pointer Interaction Button */, + 5364C0942496C22D009A9A52 /* Page Control */, + 22B7BB9718BC601D006C4AD5 /* Search */, + 53D34693265D871500D0B553 /* Segmented */, + 53D34694265D872A00D0B553 /* Slider */, + 53D428AA26826A8D001A0414 /* Stepper */, + 53D428AC26826AB1001A0414 /* Switch */, + 53D428AB26826A9C001A0414 /* TextField */, + ); + name = Controls; + sourceTree = ""; + }; + 3E2459D41931CCB5002D3369 /* Application */ = { + isa = PBXGroup; + children = ( + 2200541C18BC54E8002A6E8B /* AppDelegate.swift */, + 535D32B124970EF10011E153 /* SceneDelegate.swift */, + 5364C08A249696D7009A9A52 /* OutlineViewController.swift */, + 53F57010265761D500458712 /* CaseElement.swift */, + 530A4AB526AF352B00C0C649 /* BaseTableViewController.swift */, + ); + name = Application; + sourceTree = ""; + }; + 5364C0942496C22D009A9A52 /* Page Control */ = { + isa = PBXGroup; + children = ( + 2200543018BC54F5002A6E8B /* DefaultPageControlViewController.swift */, + 53A266AF2491ED77008EADBB /* CustomPageControlViewController.swift */, + 5364C0972496C2B3009A9A52 /* CustomPageControlViewController.storyboard */, + 5364C0952496C2B3009A9A52 /* DefaultPageControlViewController.storyboard */, + ); + name = "Page Control"; + sourceTree = ""; + }; + 53654E212298881100B999C7 /* Frameworks */ = { + isa = PBXGroup; + children = ( + DA8D372E2CBEAEDB005EBD27 /* PostHog.framework */, + 53654E222298881200B999C7 /* WebKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 53875569266093290041A1B4 /* Pointer Interaction Button */ = { + isa = PBXGroup; + children = ( + 5387556A2660933A0041A1B4 /* PointerInteractionButtonViewController.swift */, + 5387556F266095990041A1B4 /* PointerInteractionButtonViewController.storyboard */, + ); + name = "Pointer Interaction Button"; + sourceTree = ""; + }; + 53927674268684F900CB7664 /* ProgressView */ = { + isa = PBXGroup; + children = ( + 2200543218BC54F5002A6E8B /* ProgressViewController.swift */, + 53CE5AE71F2A8B1000D8A656 /* ProgressViewController.storyboard */, + ); + name = ProgressView; + sourceTree = ""; + }; + 539383DE2492800100A489A9 /* Views */ = { + isa = PBXGroup; + children = ( + 53D428AD2682C58C001A0414 /* ActivityIndicator */, + 2200541B18BC54E8002A6E8B /* AlertControllerViewController.swift */, + 539029FD1F2A53AD009775E3 /* AlertControllerViewController.storyboard */, + 2200543918BC54F5002A6E8B /* TextViewController.swift */, + 538B36F51F2A8E66002AE100 /* TextViewController.storyboard */, + 53D0549F26790A3200CD8B1A /* Image View */, + 53927674268684F900CB7664 /* ProgressView */, + B50F41071B1D284700E5147D /* StackViewController.swift */, + 53CE5AF01F2A8B8300D8A656 /* StackViewController.storyboard */, + 22B7BB9818BC6024006C4AD5 /* Toolbar */, + 531AD241265E8B1100113EC6 /* VisualEffectViewController.swift */, + 531AD243265E8B8700113EC6 /* VisualEffectViewController.storyboard */, + 2200543B18BC54F5002A6E8B /* WebViewController.swift */, + 539C6BAC1F27F4980006C5A9 /* WebViewController.storyboard */, + ); + name = Views; + sourceTree = ""; + }; + 539383DF2492801000A489A9 /* Pickers */ = { + isa = PBXGroup; + children = ( + 2200542418BC54EC002A6E8B /* DatePickerController.swift */, + 538B36F21F2A8D97002AE100 /* DatePickerController.storyboard */, + 2200543118BC54F5002A6E8B /* PickerViewController.swift */, + 53CE5AE41F2A8AEF00D8A656 /* PickerViewController.storyboard */, + 53A266B12491ED77008EADBB /* ColorPickerViewController.swift */, + 53A266BE2491ED9E008EADBB /* ColorPickerViewController.storyboard */, + 53A266B22491ED77008EADBB /* FontPickerViewController.swift */, + 53A266C02491ED9E008EADBB /* FontPickerViewController.storyboard */, + 53A266AE2491ED77008EADBB /* ImagePickerViewController.swift */, + 53A266BA2491ED9E008EADBB /* ImagePickerViewController.storyboard */, + ); + name = Pickers; + sourceTree = ""; + }; + 53CB2C5A2660038E00155325 /* Menu Button */ = { + isa = PBXGroup; + children = ( + 53CB2C61266003B200155325 /* MenuButtonViewController.swift */, + 53CB2C5E266003A800155325 /* MenuButtonViewController.storyboard */, + ); + name = "Menu Button"; + sourceTree = ""; + }; + 53D0549F26790A3200CD8B1A /* Image View */ = { + isa = PBXGroup; + children = ( + 2200542D18BC54F5002A6E8B /* ImageViewController.swift */, + 53CE5ADC1F2A8A3D00D8A656 /* ImageViewController.storyboard */, + 53D054A626790B7D00CD8B1A /* SymbolViewController.swift */, + 53D054A326790B6E00CD8B1A /* SymbolViewController.storyboard */, + ); + name = "Image View"; + sourceTree = ""; + }; + 53D34692265D86FF00D0B553 /* Button */ = { + isa = PBXGroup; + children = ( + 2200542118BC54EC002A6E8B /* ButtonViewController.swift */, + 53CB2C58265FFE9900155325 /* ButtonViewController+Configs.swift */, + 53CE5AD61F2A89E500D8A656 /* ButtonViewController.storyboard */, + ); + name = Button; + sourceTree = ""; + }; + 53D34693265D871500D0B553 /* Segmented */ = { + isa = PBXGroup; + children = ( + 2200543318BC54F5002A6E8B /* SegmentedControlViewController.swift */, + 53CE5AEA1F2A8B2F00D8A656 /* SegmentedControlViewController.storyboard */, + ); + name = Segmented; + sourceTree = ""; + }; + 53D34694265D872A00D0B553 /* Slider */ = { + isa = PBXGroup; + children = ( + 2200543418BC54F5002A6E8B /* SliderViewController.swift */, + 53CE5AED1F2A8B4F00D8A656 /* SliderViewController.storyboard */, + ); + name = Slider; + sourceTree = ""; + }; + 53D428AA26826A8D001A0414 /* Stepper */ = { + isa = PBXGroup; + children = ( + 2200543618BC54F5002A6E8B /* StepperViewController.swift */, + 53CE5AF31F2A8BB000D8A656 /* StepperViewController.storyboard */, + ); + name = Stepper; + sourceTree = ""; + }; + 53D428AB26826A9C001A0414 /* TextField */ = { + isa = PBXGroup; + children = ( + 2200543818BC54F5002A6E8B /* TextFieldViewController.swift */, + 53CE5AF91F2A8BEB00D8A656 /* TextFieldViewController.storyboard */, + ); + name = TextField; + sourceTree = ""; + }; + 53D428AC26826AB1001A0414 /* Switch */ = { + isa = PBXGroup; + children = ( + 2200543718BC54F5002A6E8B /* SwitchViewController.swift */, + 53CE5AF61F2A8BD000D8A656 /* SwitchViewController.storyboard */, + ); + name = Switch; + sourceTree = ""; + }; + 53D428AD2682C58C001A0414 /* ActivityIndicator */ = { + isa = PBXGroup; + children = ( + 2200541A18BC54E8002A6E8B /* ActivityIndicatorViewController.swift */, + 539C6BA81F27F4980006C5A9 /* ActivityIndicatorViewController.storyboard */, + ); + name = ActivityIndicator; + sourceTree = ""; + }; + D9361AEE59ED9397893793F6 /* LICENSE */ = { + isa = PBXGroup; + children = ( + 64D8CD77C30F3CED2D55B8F2 /* LICENSE.txt */, + ); + path = LICENSE; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 228DB9F218BC53F1002BA12A /* PostHogExampleAutocapture */ = { + isa = PBXNativeTarget; + buildConfigurationList = 228DBA0B18BC53F1002BA12A /* Build configuration list for PBXNativeTarget "PostHogExampleAutocapture" */; + buildPhases = ( + 228DB9EF18BC53F1002BA12A /* Sources */, + 228DB9F018BC53F1002BA12A /* Frameworks */, + 228DB9F118BC53F1002BA12A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PostHogExampleAutocapture; + productName = UIKitCatalog; + productReference = 228DB9F318BC53F1002BA12A /* PostHogExampleAutocapture.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 228DB9EB18BC53F1002BA12A /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0700; + LastUpgradeCheck = 1200; + ORGANIZATIONNAME = Apple; + TargetAttributes = { + 228DB9F218BC53F1002BA12A = { + LastSwiftMigration = 1020; + }; + }; + }; + buildConfigurationList = 228DB9EE18BC53F1002BA12A /* Build configuration list for PBXProject "PostHogExampleAutocapture" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 228DB9EA18BC53F1002BA12A; + packageReferences = ( + DA8D37322CBEAF61005EBD27 /* XCLocalSwiftPackageReference "../../posthog-ios" */, + ); + productRefGroup = 228DB9F418BC53F1002BA12A /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 228DB9F218BC53F1002BA12A /* PostHogExampleAutocapture */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 228DB9F118BC53F1002BA12A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 539C6BB01F27F4980006C5A9 /* WebViewController.storyboard in Resources */, + 5364C09A2496C2B3009A9A52 /* CustomPageControlViewController.storyboard in Resources */, + 53CE5AD81F2A89E500D8A656 /* ButtonViewController.storyboard in Resources */, + 53A266C42491ED9E008EADBB /* ColorPickerViewController.storyboard in Resources */, + 53CE5AEF1F2A8B4F00D8A656 /* SliderViewController.storyboard in Resources */, + 53A266C52491ED9E008EADBB /* FontPickerViewController.storyboard in Resources */, + 5364C0922496BEFD009A9A52 /* DefaultSearchBarViewController.storyboard in Resources */, + 53CE5AE91F2A8B1000D8A656 /* ProgressViewController.storyboard in Resources */, + 53A266C22491ED9E008EADBB /* ImagePickerViewController.storyboard in Resources */, + 5364C0992496C2B3009A9A52 /* DefaultPageControlViewController.storyboard in Resources */, + 5340A1B72496CF64004F3666 /* CustomToolbarViewController.storyboard in Resources */, + 53875571266095990041A1B4 /* PointerInteractionButtonViewController.storyboard in Resources */, + 53CE5AE61F2A8AEF00D8A656 /* PickerViewController.storyboard in Resources */, + 53B791DA1F85505400AB2FA6 /* content.html in Resources */, + 3E5C084E1974991E00969DD7 /* Main.storyboard in Resources */, + 53CE5AEC1F2A8B2F00D8A656 /* SegmentedControlViewController.storyboard in Resources */, + 5364C0932496BEFD009A9A52 /* CustomSearchBarViewController.storyboard in Resources */, + 53B791DB1F85505700AB2FA6 /* InfoPlist.strings in Resources */, + 53CB2C60266003A800155325 /* MenuButtonViewController.storyboard in Resources */, + 539C6BAE1F27F4980006C5A9 /* ActivityIndicatorViewController.storyboard in Resources */, + 53CE5AFB1F2A8BEB00D8A656 /* TextFieldViewController.storyboard in Resources */, + 53B791DC1F85505A00AB2FA6 /* Localizable.strings in Resources */, + 228DBA0818BC53F1002BA12A /* Assets.xcassets in Resources */, + 538B36F41F2A8E06002AE100 /* DatePickerController.storyboard in Resources */, + 53CE5AF21F2A8B8300D8A656 /* StackViewController.storyboard in Resources */, + 5340A1B82496CF64004F3666 /* TintedToolbarViewController.storyboard in Resources */, + 538B36F71F2A8E8A002AE100 /* TextViewController.storyboard in Resources */, + 539C6BAF1F27F4980006C5A9 /* LaunchScreen.storyboard in Resources */, + 5312D0F222848B0200048DE2 /* Credits.rtf in Resources */, + 53D054A526790B6E00CD8B1A /* SymbolViewController.storyboard in Resources */, + 539029FF1F2A53AD009775E3 /* AlertControllerViewController.storyboard in Resources */, + 53CE5AF51F2A8BB000D8A656 /* StepperViewController.storyboard in Resources */, + 531AD245265E8B8700113EC6 /* VisualEffectViewController.storyboard in Resources */, + 53CE5AF81F2A8BD000D8A656 /* SwitchViewController.storyboard in Resources */, + 53A2264B26C1AE7000C0EF3F /* Localizable.stringsdict in Resources */, + 5340A1B62496CF64004F3666 /* DefaultToolbarViewController.storyboard in Resources */, + 53CE5ADE1F2A8A3D00D8A656 /* ImageViewController.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 228DB9EF18BC53F1002BA12A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2200543F18BC54F5002A6E8B /* DefaultPageControlViewController.swift in Sources */, + 2200544918BC54F5002A6E8B /* TintedToolbarViewController.swift in Sources */, + 5364C08C249696D7009A9A52 /* OutlineViewController.swift in Sources */, + 2200544318BC54F5002A6E8B /* SliderViewController.swift in Sources */, + 2200544218BC54F5002A6E8B /* SegmentedControlViewController.swift in Sources */, + 2200544A18BC54F5002A6E8B /* WebViewController.swift in Sources */, + 2200542018BC54E8002A6E8B /* AppDelegate.swift in Sources */, + 531AD242265E8B1200113EC6 /* VisualEffectViewController.swift in Sources */, + 53CB2C62266003B200155325 /* MenuButtonViewController.swift in Sources */, + 5340A1B92496D670004F3666 /* PickerViewController.swift in Sources */, + 53A266B92491ED77008EADBB /* FontPickerViewController.swift in Sources */, + 2200541F18BC54E8002A6E8B /* AlertControllerViewController.swift in Sources */, + 2200542C18BC54EC002A6E8B /* DefaultToolbarViewController.swift in Sources */, + 2200543C18BC54F5002A6E8B /* ImageViewController.swift in Sources */, + 2200541E18BC54E8002A6E8B /* ActivityIndicatorViewController.swift in Sources */, + 2200544618BC54F5002A6E8B /* SwitchViewController.swift in Sources */, + B50F41081B1D284700E5147D /* StackViewController.swift in Sources */, + 530A4AB626AF352B00C0C649 /* BaseTableViewController.swift in Sources */, + 535D32B224970EF10011E153 /* SceneDelegate.swift in Sources */, + 53F57011265761D500458712 /* CaseElement.swift in Sources */, + 2200544118BC54F5002A6E8B /* ProgressViewController.swift in Sources */, + 2200544718BC54F5002A6E8B /* TextFieldViewController.swift in Sources */, + 2200544818BC54F5002A6E8B /* TextViewController.swift in Sources */, + 5387556B2660933A0041A1B4 /* PointerInteractionButtonViewController.swift in Sources */, + 2200542B18BC54EC002A6E8B /* DefaultSearchBarViewController.swift in Sources */, + 2200544518BC54F5002A6E8B /* StepperViewController.swift in Sources */, + 53D054A726790B7D00CD8B1A /* SymbolViewController.swift in Sources */, + 53A266B62491ED77008EADBB /* CustomPageControlViewController.swift in Sources */, + 53A266B82491ED77008EADBB /* ColorPickerViewController.swift in Sources */, + 2200542818BC54EC002A6E8B /* CustomSearchBarViewController.swift in Sources */, + 53A266B52491ED77008EADBB /* ImagePickerViewController.swift in Sources */, + 53CB2C59265FFE9900155325 /* ButtonViewController+Configs.swift in Sources */, + 2200542918BC54EC002A6E8B /* CustomToolbarViewController.swift in Sources */, + 2200542A18BC54EC002A6E8B /* DatePickerController.swift in Sources */, + 2200542718BC54EC002A6E8B /* ButtonViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 3E5C08501974991E00969DD7 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 3E5C084F1974991E00969DD7 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 5312D0F022848B0200048DE2 /* Credits.rtf */ = { + isa = PBXVariantGroup; + children = ( + 5312D0F122848B0200048DE2 /* Base */, + ); + name = Credits.rtf; + sourceTree = ""; + }; + 531AD243265E8B8700113EC6 /* VisualEffectViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 531AD244265E8B8700113EC6 /* Base */, + ); + name = VisualEffectViewController.storyboard; + sourceTree = ""; + }; + 5340A1B02496CF64004F3666 /* DefaultToolbarViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 5340A1B12496CF64004F3666 /* Base */, + ); + name = DefaultToolbarViewController.storyboard; + sourceTree = ""; + }; + 5340A1B22496CF64004F3666 /* CustomToolbarViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 5340A1B32496CF64004F3666 /* Base */, + ); + name = CustomToolbarViewController.storyboard; + sourceTree = ""; + }; + 5340A1B42496CF64004F3666 /* TintedToolbarViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 5340A1B52496CF64004F3666 /* Base */, + ); + name = TintedToolbarViewController.storyboard; + sourceTree = ""; + }; + 5364C08E2496BEFD009A9A52 /* DefaultSearchBarViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 5364C08F2496BEFD009A9A52 /* Base */, + ); + name = DefaultSearchBarViewController.storyboard; + sourceTree = ""; + }; + 5364C0902496BEFD009A9A52 /* CustomSearchBarViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 5364C0912496BEFD009A9A52 /* Base */, + ); + name = CustomSearchBarViewController.storyboard; + sourceTree = ""; + }; + 5364C0952496C2B3009A9A52 /* DefaultPageControlViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 5364C0962496C2B3009A9A52 /* Base */, + ); + name = DefaultPageControlViewController.storyboard; + sourceTree = ""; + }; + 5364C0972496C2B3009A9A52 /* CustomPageControlViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 5364C0982496C2B3009A9A52 /* Base */, + ); + name = CustomPageControlViewController.storyboard; + sourceTree = ""; + }; + 5387556F266095990041A1B4 /* PointerInteractionButtonViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 53875570266095990041A1B4 /* Base */, + ); + name = PointerInteractionButtonViewController.storyboard; + sourceTree = ""; + }; + 538B36F21F2A8D97002AE100 /* DatePickerController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 538B36F31F2A8D97002AE100 /* Base */, + ); + name = DatePickerController.storyboard; + sourceTree = ""; + }; + 538B36F51F2A8E66002AE100 /* TextViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 538B36F61F2A8E66002AE100 /* Base */, + ); + name = TextViewController.storyboard; + sourceTree = ""; + }; + 539029FD1F2A53AD009775E3 /* AlertControllerViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 539029FE1F2A53AD009775E3 /* Base */, + ); + name = AlertControllerViewController.storyboard; + sourceTree = ""; + }; + 539C6BA81F27F4980006C5A9 /* ActivityIndicatorViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 539C6BA91F27F4980006C5A9 /* Base */, + ); + name = ActivityIndicatorViewController.storyboard; + sourceTree = ""; + }; + 539C6BAA1F27F4980006C5A9 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 539C6BAB1F27F4980006C5A9 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; + 539C6BAC1F27F4980006C5A9 /* WebViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 539C6BAD1F27F4980006C5A9 /* Base */, + ); + name = WebViewController.storyboard; + sourceTree = ""; + }; + 53A2264926C1A70800C0EF3F /* Localizable.stringsdict */ = { + isa = PBXVariantGroup; + children = ( + 53A2264A26C1A70800C0EF3F /* Base */, + ); + name = Localizable.stringsdict; + sourceTree = ""; + }; + 53A266BA2491ED9E008EADBB /* ImagePickerViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 53A266BB2491ED9E008EADBB /* Base */, + ); + name = ImagePickerViewController.storyboard; + sourceTree = ""; + }; + 53A266BE2491ED9E008EADBB /* ColorPickerViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 53A266BF2491ED9E008EADBB /* Base */, + ); + name = ColorPickerViewController.storyboard; + sourceTree = ""; + }; + 53A266C02491ED9E008EADBB /* FontPickerViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 53A266C12491ED9E008EADBB /* Base */, + ); + name = FontPickerViewController.storyboard; + sourceTree = ""; + }; + 53B791D41F854B4700AB2FA6 /* content.html */ = { + isa = PBXVariantGroup; + children = ( + 53B791D51F854B4700AB2FA6 /* Base */, + ); + name = content.html; + sourceTree = ""; + }; + 53B791D61F854B4700AB2FA6 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 53B791D71F854B4700AB2FA6 /* Base */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + 53B791D81F854B4800AB2FA6 /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 53B791D91F854B4800AB2FA6 /* Base */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + 53CB2C5E266003A800155325 /* MenuButtonViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 53CB2C5F266003A800155325 /* Base */, + ); + name = MenuButtonViewController.storyboard; + sourceTree = ""; + }; + 53CE5AD61F2A89E500D8A656 /* ButtonViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 53CE5AD71F2A89E500D8A656 /* Base */, + ); + name = ButtonViewController.storyboard; + sourceTree = ""; + }; + 53CE5ADC1F2A8A3D00D8A656 /* ImageViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 53CE5ADD1F2A8A3D00D8A656 /* Base */, + ); + name = ImageViewController.storyboard; + sourceTree = ""; + }; + 53CE5AE41F2A8AEF00D8A656 /* PickerViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 53CE5AE51F2A8AEF00D8A656 /* Base */, + ); + name = PickerViewController.storyboard; + sourceTree = ""; + }; + 53CE5AE71F2A8B1000D8A656 /* ProgressViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 53CE5AE81F2A8B1000D8A656 /* Base */, + ); + name = ProgressViewController.storyboard; + sourceTree = ""; + }; + 53CE5AEA1F2A8B2F00D8A656 /* SegmentedControlViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 53CE5AEB1F2A8B2F00D8A656 /* Base */, + ); + name = SegmentedControlViewController.storyboard; + sourceTree = ""; + }; + 53CE5AED1F2A8B4F00D8A656 /* SliderViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 53CE5AEE1F2A8B4F00D8A656 /* Base */, + ); + name = SliderViewController.storyboard; + sourceTree = ""; + }; + 53CE5AF01F2A8B8300D8A656 /* StackViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 53CE5AF11F2A8B8300D8A656 /* Base */, + ); + name = StackViewController.storyboard; + sourceTree = ""; + }; + 53CE5AF31F2A8BB000D8A656 /* StepperViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 53CE5AF41F2A8BB000D8A656 /* Base */, + ); + name = StepperViewController.storyboard; + sourceTree = ""; + }; + 53CE5AF61F2A8BD000D8A656 /* SwitchViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 53CE5AF71F2A8BD000D8A656 /* Base */, + ); + name = SwitchViewController.storyboard; + sourceTree = ""; + }; + 53CE5AF91F2A8BEB00D8A656 /* TextFieldViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 53CE5AFA1F2A8BEB00D8A656 /* Base */, + ); + name = TextFieldViewController.storyboard; + sourceTree = ""; + }; + 53D054A326790B6E00CD8B1A /* SymbolViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 53D054A426790B6E00CD8B1A /* Base */, + ); + name = SymbolViewController.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 228DBA0918BC53F1002BA12A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_VERSION = ""; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 228DBA0A18BC53F1002BA12A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + SDKROOT = iphoneos; + SWIFT_VERSION = ""; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 228DBA0C18BC53F1002BA12A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = "$(SRCROOT)/PostHogExampleAutocapture/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + MARKETING_VERSION = 17; + PRODUCT_BUNDLE_IDENTIFIER = com.posthog.PostHogExampleAutocapture; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,6"; + }; + name = Debug; + }; + 228DBA0D18BC53F1002BA12A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = "$(SRCROOT)/PostHogExampleAutocapture/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + MARKETING_VERSION = 17; + PRODUCT_BUNDLE_IDENTIFIER = com.posthog.PostHogExampleAutocapture; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,6"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 228DB9EE18BC53F1002BA12A /* Build configuration list for PBXProject "PostHogExampleAutocapture" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 228DBA0918BC53F1002BA12A /* Debug */, + 228DBA0A18BC53F1002BA12A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 228DBA0B18BC53F1002BA12A /* Build configuration list for PBXNativeTarget "PostHogExampleAutocapture" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 228DBA0C18BC53F1002BA12A /* Debug */, + 228DBA0D18BC53F1002BA12A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + DA8D37322CBEAF61005EBD27 /* XCLocalSwiftPackageReference "../../posthog-ios" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = "../../posthog-ios"; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + DA8D37332CBEAF61005EBD27 /* PostHog */ = { + isa = XCSwiftPackageProductDependency; + productName = PostHog; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 228DB9EB18BC53F1002BA12A /* Project object */; +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture.xcodeproj/xcshareddata/xcschemes/PostHogExampleAutocapture.xcscheme b/PostHogExampleAutocapture/PostHogExampleAutocapture.xcodeproj/xcshareddata/xcschemes/PostHogExampleAutocapture.xcscheme new file mode 100644 index 000000000..b0e8424a2 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture.xcodeproj/xcshareddata/xcschemes/PostHogExampleAutocapture.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/ActivityIndicatorViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/ActivityIndicatorViewController.swift new file mode 100755 index 000000000..e86c3a0c2 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/ActivityIndicatorViewController.swift @@ -0,0 +1,79 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A view controller that demonstrates how to use `UIActivityIndicatorView`. + */ + +import UIKit + +class ActivityIndicatorViewController: BaseTableViewController { + // Cell identifier for each activity indicator table view cell. + enum ActivityIndicatorKind: String, CaseIterable { + case mediumIndicator + case largeIndicator + case mediumTintedIndicator + case largeTintedIndicator + } + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("MediumIndicatorTitle", comment: ""), + cellID: ActivityIndicatorKind.mediumIndicator.rawValue, + configHandler: configureMediumActivityIndicatorView), + CaseElement(title: NSLocalizedString("LargeIndicatorTitle", comment: ""), + cellID: ActivityIndicatorKind.largeIndicator.rawValue, + configHandler: configureLargeActivityIndicatorView), + ]) + + if traitCollection.userInterfaceIdiom != .mac { + // Tinted activity indicators available only on iOS. + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("MediumTintedIndicatorTitle", comment: ""), + cellID: ActivityIndicatorKind.mediumTintedIndicator.rawValue, + configHandler: configureMediumTintedActivityIndicatorView), + CaseElement(title: NSLocalizedString("LargeTintedIndicatorTitle", comment: ""), + cellID: ActivityIndicatorKind.largeTintedIndicator.rawValue, + configHandler: configureLargeTintedActivityIndicatorView), + ]) + } + } + + // MARK: - Configuration + + func configureMediumActivityIndicatorView(_ activityIndicator: UIActivityIndicatorView) { + activityIndicator.style = UIActivityIndicatorView.Style.medium + activityIndicator.hidesWhenStopped = true + + activityIndicator.startAnimating() + // When the activity is done, be sure to use UIActivityIndicatorView.stopAnimating(). + } + + func configureLargeActivityIndicatorView(_ activityIndicator: UIActivityIndicatorView) { + activityIndicator.style = UIActivityIndicatorView.Style.large + activityIndicator.hidesWhenStopped = true + + activityIndicator.startAnimating() + // When the activity is done, be sure to use UIActivityIndicatorView.stopAnimating(). + } + + func configureMediumTintedActivityIndicatorView(_ activityIndicator: UIActivityIndicatorView) { + activityIndicator.style = UIActivityIndicatorView.Style.medium + activityIndicator.hidesWhenStopped = true + activityIndicator.color = UIColor.systemPurple + + activityIndicator.startAnimating() + // When the activity is done, be sure to use UIActivityIndicatorView.stopAnimating(). + } + + func configureLargeTintedActivityIndicatorView(_ activityIndicator: UIActivityIndicatorView) { + activityIndicator.style = UIActivityIndicatorView.Style.large + activityIndicator.hidesWhenStopped = true + activityIndicator.color = UIColor.systemPurple + + activityIndicator.startAnimating() + // When the activity is done, be sure to use UIActivityIndicatorView.stopAnimating(). + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/AlertControllerViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/AlertControllerViewController.swift new file mode 100755 index 000000000..5408550b3 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/AlertControllerViewController.swift @@ -0,0 +1,314 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + The view controller that demonstrates how to use `UIAlertController`. + */ + +import UIKit + +class AlertControllerViewController: UITableViewController { + // MARK: - Properties + + weak var secureTextAlertAction: UIAlertAction? + + private enum StyleSections: Int { + case alertStyleSection = 0 + case actionStyleSection + } + + private enum AlertStyleTest: Int { + // Alert style alerts. + case showSimpleAlert = 0 + case showOkayCancelAlert + case showOtherAlert + case showTextEntryAlert + case showSecureTextEntryAlert + } + + private enum ActionSheetStyleTest: Int { + // Action sheet style alerts. + case showOkayCancelActionSheet = 0 + case howOtherActionSheet + } + + private var textDidChangeObserver: Any? = nil + + // MARK: - UIAlertControllerStyleAlert Style Alerts + + /// Show an alert with an "OK" button. + func showSimpleAlert() { + let title = NSLocalizedString("A Short Title is Best", comment: "") + let message = NSLocalizedString("A message needs to be a short, complete sentence.", comment: "") + let cancelButtonTitle = NSLocalizedString("OK", comment: "") + + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + + // Create the action. + let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .cancel) { _ in + Swift.debugPrint("The simple alert's cancel action occurred.") + } + + // Add the action. + alertController.addAction(cancelAction) + + present(alertController, animated: true, completion: nil) + } + + /// Show an alert with an "OK" and "Cancel" button. + func showOkayCancelAlert() { + let title = NSLocalizedString("A Short Title is Best", comment: "") + let message = NSLocalizedString("A message needs to be a short, complete sentence.", comment: "") + let cancelButtonTitle = NSLocalizedString("Cancel", comment: "") + let otherButtonTitle = NSLocalizedString("OK", comment: "") + + let alertCotroller = UIAlertController(title: title, message: message, preferredStyle: .alert) + + // Create the actions. + let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .cancel) { _ in + Swift.debugPrint("The \"OK/Cancel\" alert's cancel action occurred.") + } + + let otherAction = UIAlertAction(title: otherButtonTitle, style: .default) { _ in + Swift.debugPrint("The \"OK/Cancel\" alert's other action occurred.") + } + + // Add the actions. + alertCotroller.addAction(cancelAction) + alertCotroller.addAction(otherAction) + + present(alertCotroller, animated: true, completion: nil) + } + + /// Show an alert with two custom buttons. + func showOtherAlert() { + let title = NSLocalizedString("A Short Title is Best", comment: "") + let message = NSLocalizedString("A message needs to be a short, complete sentence.", comment: "") + let cancelButtonTitle = NSLocalizedString("Cancel", comment: "") + let otherButtonTitleOne = NSLocalizedString("Choice One", comment: "") + let otherButtonTitleTwo = NSLocalizedString("Choice Two", comment: "") + + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + + // Create the actions. + let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .cancel) { _ in + Swift.debugPrint("The \"Other\" alert's cancel action occurred.") + } + + let otherButtonOneAction = UIAlertAction(title: otherButtonTitleOne, style: .default) { _ in + Swift.debugPrint("The \"Other\" alert's other button one action occurred.") + } + + let otherButtonTwoAction = UIAlertAction(title: otherButtonTitleTwo, style: .default) { _ in + Swift.debugPrint("The \"Other\" alert's other button two action occurred.") + } + + // Add the actions. + alertController.addAction(cancelAction) + alertController.addAction(otherButtonOneAction) + alertController.addAction(otherButtonTwoAction) + + present(alertController, animated: true, completion: nil) + } + + /// Show a text entry alert with two custom buttons. + func showTextEntryAlert() { + let title = NSLocalizedString("A Short Title is Best", comment: "") + let message = NSLocalizedString("A message needs to be a short, complete sentence.", comment: "") + + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + + // Add the text field for text entry. + alertController.addTextField { _ in + // If you need to customize the text field, you can do so here. + } + + // Create the actions. + let cancelButtonTitle = NSLocalizedString("Cancel", comment: "") + let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .cancel) { _ in + Swift.debugPrint("The \"Text Entry\" alert's cancel action occurred.") + } + + let otherButtonTitle = NSLocalizedString("OK", comment: "") + let otherAction = UIAlertAction(title: otherButtonTitle, style: .default) { _ in + Swift.debugPrint("The \"Text Entry\" alert's other action occurred.") + } + + // Add the actions. + alertController.addAction(cancelAction) + alertController.addAction(otherAction) + + present(alertController, animated: true, completion: nil) + } + + /// Show a secure text entry alert with two custom buttons. + func showSecureTextEntryAlert() { + let title = NSLocalizedString("A Short Title is Best", comment: "") + let message = NSLocalizedString("A message needs to be a short, complete sentence.", comment: "") + let cancelButtonTitle = NSLocalizedString("Cancel", comment: "") + let otherButtonTitle = NSLocalizedString("OK", comment: "") + + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + + // Add the text field for the secure text entry. + alertController.addTextField { textField in + if let observer = self.textDidChangeObserver { + NotificationCenter.default.removeObserver(observer) + } + /** Listen for changes to the text field's text so that we can toggle the current + action's enabled property based on whether the user has entered a sufficiently + secure entry. + */ + self.textDidChangeObserver = + NotificationCenter.default.addObserver(forName: UITextField.textDidChangeNotification, + object: textField, + queue: OperationQueue.main, + using: { notification in + if let textField = notification.object as? UITextField { + // Enforce a minimum length of >= 5 characters for secure text alerts. + if let alertAction = self.secureTextAlertAction { + if let text = textField.text { + alertAction.isEnabled = text.count >= 5 + } else { + alertAction.isEnabled = false + } + } + } + }) + + textField.isSecureTextEntry = true + } + + // Create the actions. + let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .cancel) { _ in + Swift.debugPrint("The \"Secure Text Entry\" alert's cancel action occurred.") + } + + let otherAction = UIAlertAction(title: otherButtonTitle, style: .default) { _ in + Swift.debugPrint("The \"Secure Text Entry\" alert's other action occurred.") + } + + /** The text field initially has no text in the text field, so we'll disable it for now. + It will be re-enabled when the first character is typed. + */ + otherAction.isEnabled = false + + /** Hold onto the secure text alert action to toggle the enabled / disabled + state when the text changed. + */ + secureTextAlertAction = otherAction + + // Add the actions. + alertController.addAction(cancelAction) + alertController.addAction(otherAction) + + present(alertController, animated: true, completion: nil) + } + + // MARK: - UIAlertControllerStyleActionSheet Style Alerts + + // Show a dialog with an "OK" and "Cancel" button. + func showOkayCancelActionSheet(_ selectedIndexPath: IndexPath) { + let message = NSLocalizedString("A message needs to be a short, complete sentence.", comment: "") + let cancelButtonTitle = NSLocalizedString("Cancel", comment: "") + let destructiveButtonTitle = NSLocalizedString("Confirm", comment: "") + + let alertController = UIAlertController(title: nil, message: message, preferredStyle: .actionSheet) + + // Create the actions. + let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .cancel) { _ in + Swift.debugPrint("The \"OK/Cancel\" alert action sheet's cancel action occurred.") + } + + let destructiveAction = UIAlertAction(title: destructiveButtonTitle, style: .default) { _ in + Swift.debugPrint("The \"Confirm\" alert action sheet's destructive action occurred.") + } + + // Add the actions. + alertController.addAction(cancelAction) + alertController.addAction(destructiveAction) + + // Configure the alert controller's popover presentation controller if it has one. + if let popoverPresentationController = alertController.popoverPresentationController { + // Note for popovers the Cancel button is hidden automatically. + + // This method expects a valid cell to display from. + let selectedCell = tableView.cellForRow(at: selectedIndexPath)! + popoverPresentationController.sourceRect = selectedCell.frame + popoverPresentationController.sourceView = view + popoverPresentationController.permittedArrowDirections = .up + } + + present(alertController, animated: true, completion: nil) + } + + // Show a dialog with two custom buttons. + func showOtherActionSheet(_ selectedIndexPath: IndexPath) { + let message = NSLocalizedString("A message needs to be a short, complete sentence.", comment: "") + let destructiveButtonTitle = NSLocalizedString("Destructive Choice", comment: "") + let otherButtonTitle = NSLocalizedString("Safe Choice", comment: "") + + let alertController = UIAlertController(title: nil, message: message, preferredStyle: .actionSheet) + + // Create the actions. + let destructiveAction = UIAlertAction(title: destructiveButtonTitle, style: .destructive) { _ in + Swift.debugPrint("The \"Other\" alert action sheet's destructive action occurred.") + } + let otherAction = UIAlertAction(title: otherButtonTitle, style: .default) { _ in + Swift.debugPrint("The \"Other\" alert action sheet's other action occurred.") + } + + // Add the actions. + alertController.addAction(destructiveAction) + alertController.addAction(otherAction) + + // Configure the alert controller's popover presentation controller if it has one. + if let popoverPresentationController = alertController.popoverPresentationController { + // Note for popovers the Cancel button is hidden automatically. + + // This method expects a valid cell to display from. + let selectedCell = tableView.cellForRow(at: selectedIndexPath)! + popoverPresentationController.sourceRect = selectedCell.frame + popoverPresentationController.sourceView = view + popoverPresentationController.permittedArrowDirections = .up + } + + present(alertController, animated: true, completion: nil) + } +} + +// MARK: - UITableViewDelegate + +extension AlertControllerViewController { + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + switch indexPath.section { + case StyleSections.alertStyleSection.rawValue: + // Alert style. + switch indexPath.row { + case AlertStyleTest.showSimpleAlert.rawValue: + showSimpleAlert() + case AlertStyleTest.showOkayCancelAlert.rawValue: + showOkayCancelAlert() + case AlertStyleTest.showOtherAlert.rawValue: + showOtherAlert() + case AlertStyleTest.showTextEntryAlert.rawValue: + showTextEntryAlert() + case AlertStyleTest.showSecureTextEntryAlert.rawValue: + showSecureTextEntryAlert() + default: break + } + case StyleSections.actionStyleSection.rawValue: + switch indexPath.row { + // Action sheet style. + case ActionSheetStyleTest.showOkayCancelActionSheet.rawValue: + showOkayCancelActionSheet(indexPath) + case ActionSheetStyleTest.howOtherActionSheet.rawValue: + showOtherActionSheet(indexPath) + default: break + } + default: break + } + + tableView.deselectRow(at: indexPath, animated: true) + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/AppDelegate.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/AppDelegate.swift new file mode 100755 index 000000000..cfe8012d8 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/AppDelegate.swift @@ -0,0 +1,35 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + The application-specific delegate class. + */ + +import PostHog +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + let config = PostHogConfig( + apiKey: "phc_QFbR1y41s5sxnNTZoyKG2NJo2RlsCIWkUfdpawgb40D" + ) + config.debug = true + + config.captureElementInteractions = true + config.captureApplicationLifecycleEvents = true + config.sendFeatureFlagEvent = false + + config.sessionReplay = true + config.captureScreenViews = true + config.sessionReplayConfig.screenshotMode = true + config.sessionReplayConfig.maskAllTextInputs = false + config.sessionReplayConfig.maskAllImages = false + + PostHogSDK.shared.setup(config) + + PostHogSDK.shared.identify("Max Capture") + + return true + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/AppIcon.appiconset/Contents.json b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100755 index 000000000..d8db8d65f --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/Contents.json b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/Contents.json new file mode 100755 index 000000000..73c00596a --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/Flowers_1.imageset/Contents.json b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/Flowers_1.imageset/Contents.json new file mode 100755 index 000000000..4e892e187 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/Flowers_1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Flowers_1.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/Flowers_1.imageset/Flowers_1.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/Flowers_1.imageset/Flowers_1.png new file mode 100755 index 000000000..b4b3b382c Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/Flowers_1.imageset/Flowers_1.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/Flowers_2.imageset/Contents.json b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/Flowers_2.imageset/Contents.json new file mode 100755 index 000000000..f58b0f113 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/Flowers_2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Flowers_2.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/Flowers_2.imageset/Flowers_2.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/Flowers_2.imageset/Flowers_2.png new file mode 100755 index 000000000..149520fb4 Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/Flowers_2.imageset/Flowers_2.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background.imageset/Contents.json b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background.imageset/Contents.json new file mode 100755 index 000000000..5e6240639 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "stepper_and_segment_background_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "stepper_and_segment_background_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "stepper_and_segment_background_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background.imageset/stepper_and_segment_background_1x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background.imageset/stepper_and_segment_background_1x.png new file mode 100755 index 000000000..c65e3961d Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background.imageset/stepper_and_segment_background_1x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background.imageset/stepper_and_segment_background_2x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background.imageset/stepper_and_segment_background_2x.png new file mode 100755 index 000000000..6e68c5bd0 Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background.imageset/stepper_and_segment_background_2x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background.imageset/stepper_and_segment_background_3x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background.imageset/stepper_and_segment_background_3x.png new file mode 100755 index 000000000..be149037d Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background.imageset/stepper_and_segment_background_3x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background_disabled.imageset/Contents.json b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background_disabled.imageset/Contents.json new file mode 100755 index 000000000..fdb1b6672 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background_disabled.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "stepper_and_segment_background_disabled_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "stepper_and_segment_background_disabled_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "stepper_and_segment_background_disabled_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background_disabled.imageset/stepper_and_segment_background_disabled_1x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background_disabled.imageset/stepper_and_segment_background_disabled_1x.png new file mode 100755 index 000000000..7abdc2bcb Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background_disabled.imageset/stepper_and_segment_background_disabled_1x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background_disabled.imageset/stepper_and_segment_background_disabled_2x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background_disabled.imageset/stepper_and_segment_background_disabled_2x.png new file mode 100755 index 000000000..058044530 Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background_disabled.imageset/stepper_and_segment_background_disabled_2x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background_disabled.imageset/stepper_and_segment_background_disabled_3x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background_disabled.imageset/stepper_and_segment_background_disabled_3x.png new file mode 100755 index 000000000..29805f326 Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background_disabled.imageset/stepper_and_segment_background_disabled_3x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background_highlighted.imageset/Contents.json b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background_highlighted.imageset/Contents.json new file mode 100755 index 000000000..bca57e87b --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background_highlighted.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "stepper_and_segment_background_highlighted_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "stepper_and_segment_background_highlighted_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "stepper_and_segment_background_highlighted_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background_highlighted.imageset/stepper_and_segment_background_highlighted_1x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background_highlighted.imageset/stepper_and_segment_background_highlighted_1x.png new file mode 100755 index 000000000..c623650dd Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background_highlighted.imageset/stepper_and_segment_background_highlighted_1x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background_highlighted.imageset/stepper_and_segment_background_highlighted_2x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background_highlighted.imageset/stepper_and_segment_background_highlighted_2x.png new file mode 100755 index 000000000..2a9ee5c1c Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background_highlighted.imageset/stepper_and_segment_background_highlighted_2x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background_highlighted.imageset/stepper_and_segment_background_highlighted_3x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background_highlighted.imageset/stepper_and_segment_background_highlighted_3x.png new file mode 100755 index 000000000..cf0a17a54 Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/background_highlighted.imageset/stepper_and_segment_background_highlighted_3x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/search_bar_background.imageset/Contents.json b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/search_bar_background.imageset/Contents.json new file mode 100755 index 000000000..68464e93a --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/search_bar_background.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "search_bar_bg_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "search_bar_bg_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "search_bar_background_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/search_bar_background.imageset/search_bar_background_3x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/search_bar_background.imageset/search_bar_background_3x.png new file mode 100755 index 000000000..486f5413b Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/search_bar_background.imageset/search_bar_background_3x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/search_bar_background.imageset/search_bar_bg_1x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/search_bar_background.imageset/search_bar_bg_1x.png new file mode 100755 index 000000000..d20a0bb6e Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/search_bar_background.imageset/search_bar_bg_1x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/search_bar_background.imageset/search_bar_bg_2x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/search_bar_background.imageset/search_bar_bg_2x.png new file mode 100755 index 000000000..88ecb2f12 Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/search_bar_background.imageset/search_bar_bg_2x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/slider_blue_track.imageset/Contents.json b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/slider_blue_track.imageset/Contents.json new file mode 100755 index 000000000..ea6fe6474 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/slider_blue_track.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "slider_blue_track_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "slider_blue_track_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "slider_blue_track_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/slider_blue_track.imageset/slider_blue_track_1x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/slider_blue_track.imageset/slider_blue_track_1x.png new file mode 100755 index 000000000..3f1047594 Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/slider_blue_track.imageset/slider_blue_track_1x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/slider_blue_track.imageset/slider_blue_track_2x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/slider_blue_track.imageset/slider_blue_track_2x.png new file mode 100755 index 000000000..7ba361657 Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/slider_blue_track.imageset/slider_blue_track_2x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/slider_blue_track.imageset/slider_blue_track_3x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/slider_blue_track.imageset/slider_blue_track_3x.png new file mode 100755 index 000000000..7f47c6e30 Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/slider_blue_track.imageset/slider_blue_track_3x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/slider_green_track.imageset/Contents.json b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/slider_green_track.imageset/Contents.json new file mode 100755 index 000000000..bad86401d --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/slider_green_track.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "slider_green_track_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "slider_green_track_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "slider_green_track_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/slider_green_track.imageset/slider_green_track_1x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/slider_green_track.imageset/slider_green_track_1x.png new file mode 100755 index 000000000..dd6087d24 Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/slider_green_track.imageset/slider_green_track_1x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/slider_green_track.imageset/slider_green_track_2x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/slider_green_track.imageset/slider_green_track_2x.png new file mode 100755 index 000000000..5c6cd69e8 Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/slider_green_track.imageset/slider_green_track_2x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/slider_green_track.imageset/slider_green_track_3x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/slider_green_track.imageset/slider_green_track_3x.png new file mode 100755 index 000000000..75a6915a8 Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/slider_green_track.imageset/slider_green_track_3x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/stepper_and_segment_divider.imageset/Contents.json b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/stepper_and_segment_divider.imageset/Contents.json new file mode 100644 index 000000000..86976ae85 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/stepper_and_segment_divider.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "stepper_and_segment_segment_divider_1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stepper_and_segment_segment_divider_2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "stepper_and_segment_divider_3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/stepper_and_segment_divider.imageset/stepper_and_segment_divider_3x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/stepper_and_segment_divider.imageset/stepper_and_segment_divider_3x.png new file mode 100644 index 000000000..1aabd6a58 Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/stepper_and_segment_divider.imageset/stepper_and_segment_divider_3x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/stepper_and_segment_divider.imageset/stepper_and_segment_segment_divider_1x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/stepper_and_segment_divider.imageset/stepper_and_segment_segment_divider_1x.png new file mode 100644 index 000000000..2d092bd7a Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/stepper_and_segment_divider.imageset/stepper_and_segment_segment_divider_1x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/stepper_and_segment_divider.imageset/stepper_and_segment_segment_divider_2x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/stepper_and_segment_divider.imageset/stepper_and_segment_segment_divider_2x.png new file mode 100644 index 000000000..168bdfd47 Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/stepper_and_segment_divider.imageset/stepper_and_segment_segment_divider_2x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_field_background.imageset/Contents.json b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_field_background.imageset/Contents.json new file mode 100755 index 000000000..716285103 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_field_background.imageset/Contents.json @@ -0,0 +1,45 @@ +{ + "images" : [ + { + "resizing" : { + "mode" : "3-part-horizontal", + "center" : { + "mode" : "stretch", + "width" : 0 + }, + "cap-insets" : { + "right" : 1, + "left" : 1 + } + }, + "idiom" : "universal", + "filename" : "text_field_background_1x.png", + "scale" : "1x" + }, + { + "resizing" : { + "mode" : "3-part-horizontal", + "center" : { + "mode" : "stretch", + "width" : 0 + }, + "cap-insets" : { + "right" : 1, + "left" : 1 + } + }, + "idiom" : "universal", + "filename" : "text_field_background_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "text_field_background_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_field_background.imageset/text_field_background_1x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_field_background.imageset/text_field_background_1x.png new file mode 100755 index 000000000..5c3c3cf6a Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_field_background.imageset/text_field_background_1x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_field_background.imageset/text_field_background_2x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_field_background.imageset/text_field_background_2x.png new file mode 100755 index 000000000..abf9f0a01 Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_field_background.imageset/text_field_background_2x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_field_background.imageset/text_field_background_3x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_field_background.imageset/text_field_background_3x.png new file mode 100755 index 000000000..b121f9db6 Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_field_background.imageset/text_field_background_3x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_field_purple_right_view.imageset/Contents.json b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_field_purple_right_view.imageset/Contents.json new file mode 100755 index 000000000..64a5b15a8 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_field_purple_right_view.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "text_field_purple_right_view_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "text_field_purple_right_view_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "text_field_purple_right_view_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_field_purple_right_view.imageset/text_field_purple_right_view_1x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_field_purple_right_view.imageset/text_field_purple_right_view_1x.png new file mode 100755 index 000000000..c450af968 Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_field_purple_right_view.imageset/text_field_purple_right_view_1x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_field_purple_right_view.imageset/text_field_purple_right_view_2x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_field_purple_right_view.imageset/text_field_purple_right_view_2x.png new file mode 100755 index 000000000..e81719e87 Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_field_purple_right_view.imageset/text_field_purple_right_view_2x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_field_purple_right_view.imageset/text_field_purple_right_view_3x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_field_purple_right_view.imageset/text_field_purple_right_view_3x.png new file mode 100755 index 000000000..2957cbb6d Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_field_purple_right_view.imageset/text_field_purple_right_view_3x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_view_attachment.imageset/Contents.json b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_view_attachment.imageset/Contents.json new file mode 100755 index 000000000..fb8876d7a --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_view_attachment.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Sunset_5.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_view_attachment.imageset/Sunset_5.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_view_attachment.imageset/Sunset_5.png new file mode 100755 index 000000000..3ce67dff3 Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_view_attachment.imageset/Sunset_5.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_view_background.colorset/Contents.json b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_view_background.colorset/Contents.json new file mode 100755 index 000000000..e36b88e42 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/text_view_background.colorset/Contents.json @@ -0,0 +1,68 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "colors" : [ + { + "idiom" : "universal", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "1.000", + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000" + } + } + }, + { + "idiom" : "universal", + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "1.000", + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000" + } + } + }, + { + "idiom" : "universal", + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.000", + "alpha" : "0.000", + "blue" : "0.000", + "green" : "0.000" + } + } + }, + { + "idiom" : "ipad", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "1.000", + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000" + } + } + } + ] +} \ No newline at end of file diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/tinted_segmented_control.colorset/Contents.json b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/tinted_segmented_control.colorset/Contents.json new file mode 100755 index 000000000..479569c48 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/tinted_segmented_control.colorset/Contents.json @@ -0,0 +1,68 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "colors" : [ + { + "idiom" : "universal", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.031", + "alpha" : "1.000", + "blue" : "0.500", + "green" : "0.702" + } + } + }, + { + "idiom" : "universal", + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.031", + "alpha" : "1.000", + "blue" : "0.500", + "green" : "0.702" + } + } + }, + { + "idiom" : "universal", + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.209", + "alpha" : "1.000", + "blue" : "0.500", + "green" : "0.938" + } + } + }, + { + "idiom" : "ipad", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.031", + "alpha" : "1.000", + "blue" : "0.500", + "green" : "0.702" + } + } + } + ] +} \ No newline at end of file diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/tinted_stepper_control.colorset/Contents.json b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/tinted_stepper_control.colorset/Contents.json new file mode 100755 index 000000000..479569c48 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/tinted_stepper_control.colorset/Contents.json @@ -0,0 +1,68 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "colors" : [ + { + "idiom" : "universal", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.031", + "alpha" : "1.000", + "blue" : "0.500", + "green" : "0.702" + } + } + }, + { + "idiom" : "universal", + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.031", + "alpha" : "1.000", + "blue" : "0.500", + "green" : "0.702" + } + } + }, + { + "idiom" : "universal", + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.209", + "alpha" : "1.000", + "blue" : "0.500", + "green" : "0.938" + } + } + }, + { + "idiom" : "ipad", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.031", + "alpha" : "1.000", + "blue" : "0.500", + "green" : "0.702" + } + } + } + ] +} \ No newline at end of file diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/toolbar_background.imageset/Contents.json b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/toolbar_background.imageset/Contents.json new file mode 100755 index 000000000..1756a035c --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/toolbar_background.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "toolbar_background_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "toolbar_background_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "toolbar_background_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/toolbar_background.imageset/toolbar_background_1x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/toolbar_background.imageset/toolbar_background_1x.png new file mode 100755 index 000000000..f37907ff9 Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/toolbar_background.imageset/toolbar_background_1x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/toolbar_background.imageset/toolbar_background_2x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/toolbar_background.imageset/toolbar_background_2x.png new file mode 100755 index 000000000..a271d28de Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/toolbar_background.imageset/toolbar_background_2x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/toolbar_background.imageset/toolbar_background_3x.png b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/toolbar_background.imageset/toolbar_background_3x.png new file mode 100755 index 000000000..486f5413b Binary files /dev/null and b/PostHogExampleAutocapture/PostHogExampleAutocapture/Assets.xcassets/toolbar_background.imageset/toolbar_background_3x.png differ diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/ActivityIndicatorViewController.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/ActivityIndicatorViewController.storyboard new file mode 100755 index 000000000..40c0d7434 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/ActivityIndicatorViewController.storyboard @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/AlertControllerViewController.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/AlertControllerViewController.storyboard new file mode 100755 index 000000000..e52293c5a --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/AlertControllerViewController.storyboard @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/ButtonViewController.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/ButtonViewController.storyboard new file mode 100755 index 000000000..0076e70c5 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/ButtonViewController.storyboard @@ -0,0 +1,454 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/ColorPickerViewController.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/ColorPickerViewController.storyboard new file mode 100755 index 000000000..55e9ee6d7 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/ColorPickerViewController.storyboard @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/Credits.rtf b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/Credits.rtf new file mode 100755 index 000000000..c9f3ebb74 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/Credits.rtf @@ -0,0 +1,10 @@ +{\rtf1\ansi\ansicpg1252\cocoartf2617 +\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 LucidaGrande;} +{\colortbl;\red255\green255\blue255;\red0\green0\blue0;} +{\*\expandedcolortbl;;\cssrgb\c0\c0\c0\cname textColor;} +\vieww9000\viewh8400\viewkind0 +\pard\tx960\tx1920\tx2880\tx3840\tx4800\tx5760\tx6720\tx7680\tx8640\tx9600\qc\partightenfactor0 + +\f0\fs20 \cf2 Demonstrates how to use {\field{\*\fldinst{HYPERLINK "https://developer.apple.com/documentation/uikit"}}{\fldrslt UIKit}}\ +views, controls and pickers.\ +} \ No newline at end of file diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/CustomPageControlViewController.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/CustomPageControlViewController.storyboard new file mode 100755 index 000000000..6b60d8eb4 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/CustomPageControlViewController.storyboard @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/CustomSearchBarViewController.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/CustomSearchBarViewController.storyboard new file mode 100755 index 000000000..bd83634c3 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/CustomSearchBarViewController.storyboard @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/CustomToolbarViewController.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/CustomToolbarViewController.storyboard new file mode 100755 index 000000000..80366b55e --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/CustomToolbarViewController.storyboard @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/DatePickerController.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/DatePickerController.storyboard new file mode 100755 index 000000000..2d752ae3c --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/DatePickerController.storyboard @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/DefaultPageControlViewController.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/DefaultPageControlViewController.storyboard new file mode 100755 index 000000000..aac4dffef --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/DefaultPageControlViewController.storyboard @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/DefaultSearchBarViewController.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/DefaultSearchBarViewController.storyboard new file mode 100755 index 000000000..305367602 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/DefaultSearchBarViewController.storyboard @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + Title + Title + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/DefaultToolbarViewController.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/DefaultToolbarViewController.storyboard new file mode 100755 index 000000000..248ff5042 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/DefaultToolbarViewController.storyboard @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/FontPickerViewController.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/FontPickerViewController.storyboard new file mode 100755 index 000000000..b28c89f0a --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/FontPickerViewController.storyboard @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/ImagePickerViewController.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/ImagePickerViewController.storyboard new file mode 100755 index 000000000..c0c74769d --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/ImagePickerViewController.storyboard @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/ImageViewController.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/ImageViewController.storyboard new file mode 100755 index 000000000..886c07130 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/ImageViewController.storyboard @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/InfoPlist.strings b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/InfoPlist.strings new file mode 100755 index 000000000..693600d91 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +Localized versions of Info.plist keys. +*/ + +CFBundleGetInfoString = "v16.0, Copyright 2008-2021, Apple Inc."; +NSHumanReadableCopyright = "Copyright © 2008-2021, Apple Inc."; diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/LaunchScreen.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/LaunchScreen.storyboard new file mode 100755 index 000000000..24eb01561 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/Localizable.strings b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/Localizable.strings new file mode 100755 index 000000000..78c04666c --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/Localizable.strings @@ -0,0 +1,173 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +Strings used across the application via the NSLocalizedString API. +*/ + +"OK" = "OK"; +"Cancel" = "Cancel"; +"Confirm" = "Confirm"; +"Destructive Choice" = "Destructive Choice"; +"Safe Choice" = "Safe Choice"; +"A Short Title Is Best" = "A Short Title Is Best"; +"A message needs to be a short, complete sentence." = "A message needs to be a short, complete sentence."; +"Choice One" = "Choice One"; +"Choice Two" = "Choice Two"; +"Button" = "Button"; +"Pressed" = "Pressed"; +"X Button" = "X Button"; +"Image" = "Image"; +"bold" = "bold"; +"highlighted" = "highlighted"; +"underlined" = "underlined"; +"tinted" = "tinted"; +"Placeholder text" = "Placeholder text"; +"Enter search text" = "Enter search text"; +"Red color component value" = "Red color component value"; +"Green color component value" = "Green color component value"; +"Blue color component value" = "Blue color component value"; +"Animated" = "A slide show of images"; + +"Airplane" = "Airplane"; +"Gift" = "Gift"; +"Burst" = "Burst"; + +"An error occurred:" = "An error occurred:"; + +"ButtonsTitle" = "Buttons"; +"MenuButtonsTitle" = "Menu Buttons"; +"PointerInteractionButtonsTitle" = "Pointer Interaction"; +"PageControlTitle" = "Page Controls"; +"SearchBarsTitle" = "Search Bars"; +"SegmentedControlsTitle" = "Segmented Controls"; +"SlidersTitle" = "Sliders"; +"SteppersTitle" = "Steppers"; +"SwitchesTitle" = "Switches"; +"TextFieldsTitle" = "Text Fields"; + +"ActivityIndicatorsTitle" = "Activity Indicators"; +"AlertControllersTitle" = "Alert Controllers"; + +"ImagesTitle" = "Image Views"; +"ImageViewTitle" = "Image View"; +"SymbolsTitle" = "SF Symbol"; + +"ProgressViewsTitle" = "Progress Views"; +"StackViewsTitle" = "Stack Views"; +"TextViewTitle" = "Text View"; +"ToolbarsTitle" = "Toolbars"; +"VisualEffectTitle" = "Visual Effect"; +"WebViewTitle" = "Web View"; + +"DatePickerTitle" = "Date Picker"; +"PickerViewTitle" = "Picker View"; +"ColorPickerTitle" = "Color Picker"; +"FontPickerTitle" = "Font Picker"; +"ImagePickerTitle" = "Image Picker"; + +"DefaultSearchBarTitle" = "Default Search Bar"; +"CustomSearchBarTitle" = "Custom Search Bar"; + +"DefaultToolBarTitle" = "Default Toolbar"; +"TintedToolbarTitle" = "Tinted Toolbar"; +"CustomToolbarBarTitle" = "Custom Toolbar"; + +"ChooseItemTitle" = "Choose an item:"; +"ItemTitle" = "Item %@"; + +"SampleFontTitle" = "Sample Font"; + +"CheckTitle" = "Check"; +"SearchTitle" = "Search"; +"ToolsTitle" = "Tools"; + +"DefaultPageControlTitle" = "Page Control"; +"CustomPageControlTitle" = "Custom Page Control"; + +"SwitchTitle" = "Title"; + +"DefaultSwitchTitle" = "Default"; +"CheckboxSwitchTitle" = "Checkbox"; +"TintedSwitchTitle" = "Tinted"; + +"ImageToolTipTitle" = "This is a list of flower photos obtained from the sample's asset library."; +"GrayStyleButtonToolTipTitle" = "This is a gray-style system button."; +"TintedStyleButtonToolTipTitle" = "This is a tinted-style system button."; +"FilledStyleButtonToolTipTitle" = "This is a filled-style system button."; +"CapsuleStyleButtonToolTipTitle" = "This is a capsule-style system button."; +"CartFilledButtonToolTipTitle" = "Button cart is filled"; +"CartEmptyButtonToolTipTitle" = "Button cart is empty"; +"XButtonToolTipTitle" = "X Button"; +"PersonButtonToolTipTitle" = "Person Button"; +"VisualEffectToolTipTitle" = "This demonstrates how to use a UIVisualEffectView on top of an UIImageView and underneath a UITextView."; + +"VisualEffectTextContent" = "This is a UITextView with text content placed inside a UIVisualEffectView. This is a UITextView with text content placed inside a UIVisualEffectView. This is a UITextView with text content placed inside a UIVisualEffectView."; + +"DefaultTitle" = "Default"; +"DetailDisclosureTitle" = "Detail Disclosure"; +"AddContactTitle" = "Add Contact"; +"CloseTitle" = "Close"; +"GrayTitle" = "Gray"; +"TintedTitle" = "Tinted"; +"FilledTitle" = "Filled"; +"CornerStyleTitle" = "Corner Style"; +"ToggleTitle" = "Toggle"; +"ButtonColorTitle" = "Colored Title"; + +"ImageTitle" = "Image"; +"AttributedStringTitle" = "Attributed String"; +"SymbolTitle" = "Symbol"; + +"LargeSymbolTitle" = "Large Symbol"; +"SymbolStringTitle" = "Symbol + String"; +"StringSymbolTitle" = "String + Symbol"; +"MultiTitleTitle" = "Multi-Title"; +"BackgroundTitle" = "Background"; + +"UpdateActivityHandlerTitle" = "Update Activity Handler"; +"UpdateHandlerTitle" = "Update Handler"; +"UpdateImageHandlerTitle" = "Update Handler (Button Image)"; + +"AddToCartTitle" = "Add to Cart"; + +"DropDownTitle" = "Drop Down"; +"DropDownProgTitle" = "Drop Down Programmatic"; +"DropDownMultiActionTitle" = "Drop Down Multi-Action"; +"DropDownButtonSubMenuTitle" = "Drop Down Submenu"; +"PopupSelection" = "Popup Selection"; +"PopupMenuTitle" = "Popup Menu"; + +"CustomSegmentsTitle" = "Custom Segments"; +"CustomBackgroundTitle" = "Custom Background"; +"ActionBasedTitle" = "Action Based"; + +"CustomTitle" = "Custom"; +"MinMaxImagesTitle" = "Min and Max Images"; + +"DefaultStepperTitle" = "Default Stepper"; +"TintedStepperTitle" = "Tinted Stepper"; +"CustomStepperTitle" = "Custom Stepper"; + +"PlainSymbolTitle" = "Default"; +"TintedSymbolTitle" = "Tinted"; +"LargeSymbolTitle" = "Large"; +"HierarchicalSymbolTitle" = "Hierarchical Color"; +"PaletteSymbolTitle" = "Palette Color"; +"PreferringMultiColorSymbolTitle" = "Preferring Multi-Color"; + +"DefaultTextFieldTitle" = "Default"; +"TintedTextFieldTitle" = "Tinted"; +"SecuretTextFieldTitle" = "Secure"; +"SpecificKeyboardTextFieldTitle" = "Specific Keyboard"; +"CustomTextFieldTitle" = "Custom"; +"SearchTextFieldTitle" = "Search"; + +"MediumIndicatorTitle" = "Medium"; +"LargeIndicatorTitle" = "Large"; +"MediumTintedIndicatorTitle" = "Medium Tinted"; +"LargeTintedIndicatorTitle" = "Large Tinted"; + +"ProgressDefaultTitle" = "Default"; +"ProgressBarTitle" = "Bar"; +"ProgressTintedTitle" = "Tinted"; diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/Main.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/Main.storyboard new file mode 100755 index 000000000..c2e9193bc --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/Main.storyboard @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/MenuButtonViewController.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/MenuButtonViewController.storyboard new file mode 100755 index 000000000..6e7ecf37c --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/MenuButtonViewController.storyboard @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/PickerViewController.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/PickerViewController.storyboard new file mode 100755 index 000000000..f5209519a --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/PickerViewController.storyboard @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/PointerInteractionButtonViewController.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/PointerInteractionButtonViewController.storyboard new file mode 100755 index 000000000..664719dc7 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/PointerInteractionButtonViewController.storyboard @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/ProgressViewController.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/ProgressViewController.storyboard new file mode 100755 index 000000000..efc642095 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/ProgressViewController.storyboard @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/SegmentedControlViewController.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/SegmentedControlViewController.storyboard new file mode 100755 index 000000000..4166c5b05 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/SegmentedControlViewController.storyboard @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/SliderViewController.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/SliderViewController.storyboard new file mode 100755 index 000000000..4420a0f00 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/SliderViewController.storyboard @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/StackViewController.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/StackViewController.storyboard new file mode 100755 index 000000000..a3b3d8872 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/StackViewController.storyboard @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/StepperViewController.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/StepperViewController.storyboard new file mode 100755 index 000000000..a28bc8f7d --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/StepperViewController.storyboard @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/SwitchViewController.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/SwitchViewController.storyboard new file mode 100755 index 000000000..69655b053 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/SwitchViewController.storyboard @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/SymbolViewController.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/SymbolViewController.storyboard new file mode 100755 index 000000000..cecdae810 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/SymbolViewController.storyboard @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/TextFieldViewController.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/TextFieldViewController.storyboard new file mode 100755 index 000000000..502ef54eb --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/TextFieldViewController.storyboard @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/TextViewController.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/TextViewController.storyboard new file mode 100755 index 000000000..df145e154 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/TextViewController.storyboard @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + This is a UITextView that uses attributed text. You can programmatically modify the display of the text by making it bold, highlighted, underlined, tinted, symbols, and more. These attributes are defined in NSAttributedString.h. You can even embed attachments in an NSAttributedString! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/TintedToolbarViewController.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/TintedToolbarViewController.storyboard new file mode 100755 index 000000000..b5b460b35 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/TintedToolbarViewController.storyboard @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/VisualEffectViewController.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/VisualEffectViewController.storyboard new file mode 100755 index 000000000..12d43a517 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/VisualEffectViewController.storyboard @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/WebViewController.storyboard b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/WebViewController.storyboard new file mode 100755 index 000000000..d335aaaa1 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/WebViewController.storyboard @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/content.html b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/content.html new file mode 100755 index 000000000..c2dc89958 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Base.lproj/content.html @@ -0,0 +1,16 @@ + + + + WKWebView + + + +
+

This is HTML content inside a WKWebView.

+ For more information refer to developer.apple.com + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/BaseTableViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/BaseTableViewController.swift new file mode 100644 index 000000000..ba89bfb9b --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/BaseTableViewController.swift @@ -0,0 +1,52 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A base class used for all UITableViewControllers in this sample app. + */ + +import UIKit + +class BaseTableViewController: UITableViewController { + // List of table view cell test cases. + var testCells = [CaseElement]() + + func centeredHeaderView(_ title: String) -> UITableViewHeaderFooterView { + // Set the header title and make it centered. + let headerView = UITableViewHeaderFooterView() + var content = UIListContentConfiguration.groupedHeader() + content.text = title + content.textProperties.alignment = .center + headerView.contentConfiguration = content + return headerView + } + + // MARK: - UITableViewDataSource + + override func tableView(_: UITableView, viewForHeaderInSection section: Int) -> UIView? { + centeredHeaderView(testCells[section].title) + } + + override func tableView(_: UITableView, titleForHeaderInSection section: Int) -> String? { + testCells[section].title + } + + override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { + 1 + } + + override func numberOfSections(in _: UITableView) -> Int { + testCells.count + } + + override func tableView(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell + { + let cellTest = testCells[indexPath.section] + let cell = tableView.dequeueReusableCell(withIdentifier: cellTest.cellID, for: indexPath) + if let view = cellTest.targetView(cell) { + cellTest.configHandler(view) + } + return cell + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/ButtonViewController+Configs.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/ButtonViewController+Configs.swift new file mode 100755 index 000000000..ca13b815a --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/ButtonViewController+Configs.swift @@ -0,0 +1,468 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + Configuration functions for all the UIButtons found in ButtonViewController. + */ + +import UIKit + +extension ButtonViewController: UIToolTipInteractionDelegate { + func configureSystemTextButton(_ button: UIButton) { + button.setTitle(NSLocalizedString("Button", comment: ""), for: []) + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + func configureSystemDetailDisclosureButton(_ button: UIButton) { + // Nothing particular to set here, it's all been done in the storyboard. + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + func configureSystemContactAddButton(_ button: UIButton) { + // Nothing particular to set here, it's all been done in the storyboard. + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + func configureCloseButton(_ button: UIButton) { + // Nothing particular to set here, it's all been done in the storyboard. + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + @available(iOS 15.0, *) + func configureStyleGrayButton(_ button: UIButton) { + // Note this can be also be done in the storyboard for this button. + let config = UIButton.Configuration.gray() + button.configuration = config + + button.setTitle(NSLocalizedString("Button", comment: ""), for: .normal) + button.toolTip = NSLocalizedString("GrayStyleButtonToolTipTitle", comment: "") + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + @available(iOS 15.0, *) + func configureStyleTintedButton(_ button: UIButton) { + // Note this can be also be done in the storyboard for this button. + + var config = UIButton.Configuration.tinted() + + /** To keep the look the same betwen iOS and macOS: + For tinted color to work in Mac Catalyst, use UIBehavioralStyle as ".pad", + Available in macOS 12 or later (Mac Catalyst 15.0 or later). + Use this for controls that need to look the same between iOS and macOS. + */ + if traitCollection.userInterfaceIdiom == .mac { + button.preferredBehavioralStyle = .pad + } + + // The following will make the button title red and background a lighter red. + config.baseBackgroundColor = .systemRed + config.baseForegroundColor = .systemRed + + button.setTitle(NSLocalizedString("Button", comment: ""), for: .normal) + button.toolTip = NSLocalizedString("TintedStyleButtonToolTipTitle", comment: "") + + button.configuration = config + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + @available(iOS 15.0, *) + func configureStyleFilledButton(_ button: UIButton) { + // Note this can be also be done in the storyboard for this button. + var config = UIButton.Configuration.filled() + config.background.backgroundColor = .systemRed + button.configuration = config + + button.setTitle(NSLocalizedString("Button", comment: ""), for: .normal) + button.toolTip = NSLocalizedString("FilledStyleButtonToolTipTitle", comment: "") + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + @available(iOS 15.0, *) + func configureCornerStyleButton(_ button: UIButton) { + /** To keep the look the same betwen iOS and macOS: + For cornerStyle to work in Mac Catalyst, use UIBehavioralStyle as ".pad", + Available in macOS 12 or later (Mac Catalyst 15.0 or later). + Use this for controls that need to look the same between iOS and macOS. + */ + if traitCollection.userInterfaceIdiom == .mac { + button.preferredBehavioralStyle = .pad + } + + var config = UIButton.Configuration.gray() + config.cornerStyle = .capsule + button.configuration = config + + button.setTitle(NSLocalizedString("Button", comment: ""), for: .normal) + button.toolTip = NSLocalizedString("CapsuleStyleButtonToolTipTitle", comment: "") + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + func configureImageButton(_ button: UIButton) { + // To create this button in code you can use `UIButton.init(type: .system)`. + + // Set the tint color to the button's image. + if let image = UIImage(systemName: "xmark") { + let imageButtonNormalImage = image.withTintColor(.systemPurple) + button.setImage(imageButtonNormalImage, for: .normal) + } + + // Since this button title is just an image, add an accessibility label. + button.accessibilityLabel = NSLocalizedString("X", comment: "") + + if #available(iOS 15, *) { + button.toolTip = NSLocalizedString("XButtonToolTipTitle", comment: "") + } + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + func configureAttributedTextSystemButton(_ button: UIButton) { + let buttonTitle = NSLocalizedString("Button", comment: "") + + // Set the button's title for normal state. + let normalTitleAttributes: [NSAttributedString.Key: Any] = [ + NSAttributedString.Key.strikethroughStyle: NSUnderlineStyle.single.rawValue, + ] + + let normalAttributedTitle = NSAttributedString(string: buttonTitle, attributes: normalTitleAttributes) + button.setAttributedTitle(normalAttributedTitle, for: .normal) + + // Set the button's title for highlighted state (note this is not supported in Mac Catalyst). + let highlightedTitleAttributes: [NSAttributedString.Key: Any] = [ + NSAttributedString.Key.foregroundColor: UIColor.systemGreen, + NSAttributedString.Key.strikethroughStyle: NSUnderlineStyle.thick.rawValue, + ] + let highlightedAttributedTitle = NSAttributedString(string: buttonTitle, attributes: highlightedTitleAttributes) + button.setAttributedTitle(highlightedAttributedTitle, for: .highlighted) + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + func configureSymbolButton(_ button: UIButton) { + let buttonImage = UIImage(systemName: "person") + + if #available(iOS 15, *) { + // For iOS 15 use the UIButtonConfiguration to set the image. + var buttonConfig = UIButton.Configuration.plain() + buttonConfig.image = buttonImage + button.configuration = buttonConfig + + button.toolTip = NSLocalizedString("PersonButtonToolTipTitle", comment: "") + } else { + button.setImage(buttonImage, for: .normal) + } + + let config = UIImage.SymbolConfiguration(textStyle: .body, scale: .large) + button.setPreferredSymbolConfiguration(config, forImageIn: .normal) + + // Since this button title is just an image, add an accessibility label. + button.accessibilityLabel = NSLocalizedString("Person", comment: "") + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + func configureLargeSymbolButton(_ button: UIButton) { + let buttonImage = UIImage(systemName: "person") + + if #available(iOS 15, *) { + // For iOS 15 use the UIButtonConfiguration to change the size. + var buttonConfig = UIButton.Configuration.plain() + buttonConfig.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(textStyle: .largeTitle) + buttonConfig.image = buttonImage + button.configuration = buttonConfig + } else { + button.setImage(buttonImage, for: .normal) + } + + // Since this button title is just an image, add an accessibility label. + button.accessibilityLabel = NSLocalizedString("Person", comment: "") + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + func configureSymbolTextButton(_ button: UIButton) { + // Button with image to the left of the title. + + let buttonImage = UIImage(systemName: "person") + + if #available(iOS 15, *) { + // Use UIButtonConfiguration to set the image. + var buttonConfig = UIButton.Configuration.plain() + + // Set up the symbol image size to match that of the title font size. + buttonConfig.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(textStyle: .body) + buttonConfig.image = buttonImage + + button.configuration = buttonConfig + } else { + button.setImage(buttonImage, for: .normal) + + // Set up the symbol image size to match that of the title font size. + let config = UIImage.SymbolConfiguration(textStyle: .body, scale: .small) + button.setPreferredSymbolConfiguration(config, forImageIn: .normal) + } + + // Set the button's title and font. + button.setTitle(NSLocalizedString("Person", comment: ""), for: []) + button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body) + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + func configureTextSymbolButton(_ button: UIButton) { + // Button with image to the right of the title. + + let buttonImage = UIImage(systemName: "person") + + if #available(iOS 15, *) { + // Use UIButtonConfiguration to set the image. + var buttonConfig = UIButton.Configuration.plain() + + // Set up the symbol image size to match that of the title font size. + buttonConfig.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(textStyle: .body) + + buttonConfig.image = buttonImage + + // Set the image placement to the right of the title. + /** For image placement to work in Mac Catalyst, use UIBehavioralStyle as ".pad", + Available in macOS 12 or later (Mac Catalyst 15.0 or later). + Use this for controls that need to look the same between iOS and macOS. + */ + if traitCollection.userInterfaceIdiom == .mac { + button.preferredBehavioralStyle = .pad + } + buttonConfig.imagePlacement = .trailing + + button.configuration = buttonConfig + } + + // Set the button's title and font. + button.setTitle(NSLocalizedString("Person", comment: ""), for: []) + button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body) + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + @available(iOS 15.0, *) + func configureMultiTitleButton(_ button: UIButton) { + /** To keep the look the same betwen iOS and macOS: + For setTitle(.highlighted) to work in Mac Catalyst, use UIBehavioralStyle as ".pad", + Available in macOS 12 or later (Mac Catalyst 15.0 or later). + Use this for controls that need to look the same between iOS and macOS. + */ + if traitCollection.userInterfaceIdiom == .mac { + button.preferredBehavioralStyle = .pad + } + + button.setTitle(NSLocalizedString("Button", comment: ""), for: .normal) + button.setTitle(NSLocalizedString("Pressed", comment: ""), for: .highlighted) + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + @available(iOS 15.0, *) + func configureToggleButton(button: UIButton) { + button.changesSelectionAsPrimaryAction = true // This makes the button style a "toggle button". + } + + func configureTitleTextButton(_ button: UIButton) { + // Note: Only for iOS the title's color can be changed. + button.setTitleColor(UIColor.systemGreen, for: [.normal]) + button.setTitleColor(UIColor.systemRed, for: [.highlighted]) + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + func configureBackgroundButton(_ button: UIButton) { + if #available(iOS 15, *) { + /** To keep the look the same betwen iOS and macOS: + For setBackgroundImage to work in Mac Catalyst, use UIBehavioralStyle as ".pad", + Available in macOS 12 or later (Mac Catalyst 15.0 or later). + Use this for controls that need to look the same between iOS and macOS. + */ + if traitCollection.userInterfaceIdiom == .mac { + button.preferredBehavioralStyle = .pad + } + } + + button.setBackgroundImage(UIImage(named: "background"), for: .normal) + button.setBackgroundImage(UIImage(named: "background_highlighted"), for: .highlighted) + button.setBackgroundImage(UIImage(named: "background_disabled"), for: .disabled) + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + // This handler is called when this button needs updating. + @available(iOS 15.0, *) + func configureUpdateActivityHandlerButton(_ button: UIButton) { + let activityUpdateHandler: (UIButton) -> Void = { button in + /// Shows an activity indicator in place of an image. Its placement is controlled by the `imagePlacement` property. + + // Start with the current button's configuration. + var config = button.configuration + config?.showsActivityIndicator = button.isSelected ? false : true + button.configuration = config + } + + var buttonConfig = UIButton.Configuration.plain() + buttonConfig.image = UIImage(systemName: "tray") + buttonConfig.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(textStyle: .body) + button.configuration = buttonConfig + + // Set the button's title and font. + button.setTitle(NSLocalizedString("Button", comment: ""), for: []) + button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body) + + button.changesSelectionAsPrimaryAction = true // This turns on the toggle behavior. + button.configurationUpdateHandler = activityUpdateHandler + + // For this button to include an activity indicator next to the title, keep the iPad behavior. + if traitCollection.userInterfaceIdiom == .mac { + button.preferredBehavioralStyle = .pad + } + + button.addTarget(self, action: #selector(ButtonViewController.toggleButtonClicked(_:)), for: .touchUpInside) + } + + @available(iOS 15.0, *) + func configureUpdateHandlerButton(_ button: UIButton) { + // This is called when a button needs an update. + let colorUpdateHandler: (UIButton) -> Void = { button in + button.configuration?.baseBackgroundColor = button.isSelected + ? UIColor.systemPink.withAlphaComponent(0.4) + : UIColor.systemPink + } + + let buttonConfig = UIButton.Configuration.filled() + button.configuration = buttonConfig + + button.changesSelectionAsPrimaryAction = true // This turns on the toggle behavior. + button.configurationUpdateHandler = colorUpdateHandler + + // For this button to use baseBackgroundColor for the visual toggle state, keep the iPad behavior. + if traitCollection.userInterfaceIdiom == .mac { + button.preferredBehavioralStyle = .pad + } + + button.addTarget(self, action: #selector(ButtonViewController.toggleButtonClicked(_:)), for: .touchUpInside) + } + + @available(iOS 15.0, *) + func configureUpdateImageHandlerButton(_ button: UIButton) { + // This is called when a button needs an update. + let colorUpdateHandler: (UIButton) -> Void = { button in + button.configuration?.image = + button.isSelected ? UIImage(systemName: "cart.fill") : UIImage(systemName: "cart") + button.toolTip = + button.isSelected ? + NSLocalizedString("CartFilledButtonToolTipTitle", comment: "") : + NSLocalizedString("CartEmptyButtonToolTipTitle", comment: "") + } + + var buttonConfig = UIButton.Configuration.plain() + buttonConfig.image = UIImage(systemName: "cart") + buttonConfig.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(textStyle: .largeTitle) + button.configuration = buttonConfig + + button.changesSelectionAsPrimaryAction = true // This turns on the toggle behavior. + button.configurationUpdateHandler = colorUpdateHandler + + // For this button to use the updateHandler to change it's icon for the visual toggle state, keep the iPad behavior. + if traitCollection.userInterfaceIdiom == .mac { + button.preferredBehavioralStyle = .pad + } + + button.setTitle("", for: []) // No title, just an image. + button.isSelected = false + + button.addTarget(self, action: #selector(ButtonViewController.toggleButtonClicked(_:)), for: .touchUpInside) + } + + // MARK: - Add To Cart Button + + @available(iOS 15.0, *) + func toolTipInteraction(_: UIToolTipInteraction, configurationAt _: CGPoint) -> UIToolTipConfiguration? { + let formatString = NSLocalizedString("Cart Tooltip String", + comment: "Cart Tooltip String format to be found in Localizable.stringsdict") + let resultString = String.localizedStringWithFormat(formatString, cartItemCount) + return UIToolTipConfiguration(toolTip: resultString) + } + + @available(iOS 15.0, *) + func addToCart(action: UIAction) { + cartItemCount = cartItemCount > 0 ? 0 : 12 + if let button = action.sender as? UIButton { + button.setNeedsUpdateConfiguration() + } + } + + @available(iOS 15.0, *) + func configureAddToCartButton(_ button: UIButton) { + var config = UIButton.Configuration.filled() + config.buttonSize = .large + config.image = UIImage(systemName: "cart.fill") + config.title = "Add to Cart" + config.cornerStyle = .capsule + config.baseBackgroundColor = UIColor.systemTeal + button.configuration = config + + button.toolTip = "" // The value will be determined in its delegate. + button.toolTipInteraction?.delegate = self + + button.addAction(UIAction(handler: addToCart(action:)), for: .touchUpInside) + + // For this button to include subtitle and larger size, the behavioral style needs to be set to ".pad". + if traitCollection.userInterfaceIdiom == .mac { + button.preferredBehavioralStyle = .pad + } + + button.changesSelectionAsPrimaryAction = true // This turns on the toggle behavior. + + // This handler is called when this button needs updating. + button.configurationUpdateHandler = { + [unowned self] button in + + // Start with the current button's configuration. + var newConfig = button.configuration + + if button.isSelected { + // The button was clicked or tapped. + newConfig?.image = cartItemCount > 0 + ? UIImage(systemName: "cart.fill.badge.plus") + : UIImage(systemName: "cart.badge.plus") + + let formatString = NSLocalizedString("Cart Items String", + comment: "Cart Items String format to be found in Localizable.stringsdict") + let resultString = String.localizedStringWithFormat(formatString, cartItemCount) + newConfig?.subtitle = resultString + } else { + // As the button is highlighted (pressed), apply a temporary image and subtitle. + newConfig?.image = UIImage(systemName: "cart.fill") + newConfig?.subtitle = "" + } + + newConfig?.imagePadding = 8 // Add a litle more space between the icon and button title. + + // Note: To change the padding between the title and subtitle, set "titlePadding". + // Note: To change the padding around the perimeter of the button, set "contentInsets". + + button.configuration = newConfig + } + } + + // MARK: - Button Actions + + @objc + func buttonClicked(_: UIButton) { + Swift.debugPrint("Button was clicked.") + } + + @objc + func toggleButtonClicked(_ sender: UIButton) { + Swift.debugPrint("Toggle action: \(sender)") + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/ButtonViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/ButtonViewController.swift new file mode 100755 index 000000000..019d449cb --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/ButtonViewController.swift @@ -0,0 +1,154 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A view controller that demonstrates how to use `UIButton`. + The buttons are created using storyboards, but each of the system buttons can be created in code by + using the UIButton.init(type buttonType: UIButtonType) initializer. + + See the UIButton interface for a comprehensive list of the various UIButtonType values. + */ + +import UIKit + +class ButtonViewController: BaseTableViewController { + // Cell identifier for each button table view cell. + enum ButtonKind: String, CaseIterable { + case buttonSystem + case buttonDetailDisclosure + case buttonSystemAddContact + case buttonClose + case buttonStyleGray + case buttonStyleTinted + case buttonStyleFilled + case buttonCornerStyle + case buttonToggle + case buttonTitleColor + case buttonImage + case buttonAttrText + case buttonSymbol + case buttonLargeSymbol + case buttonTextSymbol + case buttonSymbolText + case buttonMultiTitle + case buttonBackground + case addToCartButton + case buttonUpdateActivityHandler + case buttonUpdateHandler + case buttonImageUpdateHandler + } + + // MARK: - Properties + + // "Add to Cart" Button + var cartItemCount: Int = 0 + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("DefaultTitle", comment: ""), + cellID: ButtonKind.buttonSystem.rawValue, + configHandler: configureSystemTextButton), + CaseElement(title: NSLocalizedString("DetailDisclosureTitle", comment: ""), + cellID: ButtonKind.buttonDetailDisclosure.rawValue, + configHandler: configureSystemDetailDisclosureButton), + CaseElement(title: NSLocalizedString("AddContactTitle", comment: ""), + cellID: ButtonKind.buttonSystemAddContact.rawValue, + configHandler: configureSystemContactAddButton), + CaseElement(title: NSLocalizedString("CloseTitle", comment: ""), + cellID: ButtonKind.buttonClose.rawValue, + configHandler: configureCloseButton), + ]) + + if #available(iOS 15, *) { + // These button styles are available on iOS 15 or later. + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("GrayTitle", comment: ""), + cellID: ButtonKind.buttonStyleGray.rawValue, + configHandler: configureStyleGrayButton), + CaseElement(title: NSLocalizedString("TintedTitle", comment: ""), + cellID: ButtonKind.buttonStyleTinted.rawValue, + configHandler: configureStyleTintedButton), + CaseElement(title: NSLocalizedString("FilledTitle", comment: ""), + cellID: ButtonKind.buttonStyleFilled.rawValue, + configHandler: configureStyleFilledButton), + CaseElement(title: NSLocalizedString("CornerStyleTitle", comment: ""), + cellID: ButtonKind.buttonCornerStyle.rawValue, + configHandler: configureCornerStyleButton), + CaseElement(title: NSLocalizedString("ToggleTitle", comment: ""), + cellID: ButtonKind.buttonToggle.rawValue, + configHandler: configureToggleButton), + ]) + } + + if traitCollection.userInterfaceIdiom != .mac { + // Colored button titles only on iOS. + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("ButtonColorTitle", comment: ""), + cellID: ButtonKind.buttonTitleColor.rawValue, + configHandler: configureTitleTextButton), + ]) + } + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("ImageTitle", comment: ""), + cellID: ButtonKind.buttonImage.rawValue, + configHandler: configureImageButton), + CaseElement(title: NSLocalizedString("AttributedStringTitle", comment: ""), + cellID: ButtonKind.buttonAttrText.rawValue, + configHandler: configureAttributedTextSystemButton), + CaseElement(title: NSLocalizedString("SymbolTitle", comment: ""), + cellID: ButtonKind.buttonSymbol.rawValue, + configHandler: configureSymbolButton), + ]) + + if #available(iOS 15, *) { + // This case uses UIButtonConfiguration which is available on iOS 15 or later. + if traitCollection.userInterfaceIdiom != .mac { + // UIButtonConfiguration for large images available only on iOS. + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("LargeSymbolTitle", comment: ""), + cellID: ButtonKind.buttonLargeSymbol.rawValue, + configHandler: configureLargeSymbolButton), + ]) + } + } + + if #available(iOS 15, *) { + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("StringSymbolTitle", comment: ""), + cellID: ButtonKind.buttonTextSymbol.rawValue, + configHandler: configureTextSymbolButton), + CaseElement(title: NSLocalizedString("SymbolStringTitle", comment: ""), + cellID: ButtonKind.buttonSymbolText.rawValue, + configHandler: configureSymbolTextButton), + + CaseElement(title: NSLocalizedString("BackgroundTitle", comment: ""), + cellID: ButtonKind.buttonBackground.rawValue, + configHandler: configureBackgroundButton), + + // Multi-title button: title for normal and highlight state, setTitle(.highlighted) is for iOS 15 and later. + CaseElement(title: NSLocalizedString("MultiTitleTitle", comment: ""), + cellID: ButtonKind.buttonMultiTitle.rawValue, + configHandler: configureMultiTitleButton), + + // Various button effects done to the addToCartButton are available only on iOS 15 or later. + CaseElement(title: NSLocalizedString("AddToCartTitle", comment: ""), + cellID: ButtonKind.addToCartButton.rawValue, + configHandler: configureAddToCartButton), + + // UIButtonConfiguration with updateHandlers is available only on iOS 15 or later. + CaseElement(title: NSLocalizedString("UpdateActivityHandlerTitle", comment: ""), + cellID: ButtonKind.buttonUpdateActivityHandler.rawValue, + configHandler: configureUpdateActivityHandlerButton), + CaseElement(title: NSLocalizedString("UpdateHandlerTitle", comment: ""), + cellID: ButtonKind.buttonUpdateHandler.rawValue, + configHandler: configureUpdateHandlerButton), + CaseElement(title: NSLocalizedString("UpdateImageHandlerTitle", comment: ""), + cellID: ButtonKind.buttonImageUpdateHandler.rawValue, + configHandler: configureUpdateImageHandlerButton), + ]) + } + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/CaseElement.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/CaseElement.swift new file mode 100644 index 000000000..453f0489f --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/CaseElement.swift @@ -0,0 +1,29 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + Test case element that serves our UITableViewCells. + */ + +import UIKit + +struct CaseElement { + var title: String // Visual title of the cell (table section header title) + var cellID: String // Table view cell's identifier for searching for the cell within the nib file. + + typealias ConfigurationClosure = (UIView) -> Void + var configHandler: ConfigurationClosure // Configuration handler for setting up the cell's subview. + + init(title: String, cellID: String, configHandler: @escaping (V) -> Void) { + self.title = title + self.cellID = cellID + self.configHandler = { view in + guard let view = view as? V else { fatalError("Impossible") } + configHandler(view) + } + } + + func targetView(_ cell: UITableViewCell?) -> UIView? { + cell != nil ? cell!.contentView.subviews[0] : nil + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/ColorPickerViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/ColorPickerViewController.swift new file mode 100755 index 000000000..5f9da5104 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/ColorPickerViewController.swift @@ -0,0 +1,141 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A view controller that demonstrates how to use `UIColorPickerViewController`. + */ + +import UIKit + +class ColorPickerViewController: UIViewController, UIColorPickerViewControllerDelegate { + // MARK: - Properties + + var colorWell: UIColorWell! + var colorPicker: UIColorPickerViewController! + + @IBOutlet var pickerButton: UIButton! // UIButton to present the picker. + @IBOutlet var pickerWellView: UIView! // UIView placeholder to hold the UIColorWell. + + @IBOutlet var colorView: UIView! + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + configureColorPicker() + configureColorWell() + + // For iOS, the picker button in the main view is not used, the color picker is presented from the navigation bar. + if navigationController?.traitCollection.userInterfaceIdiom != .mac { + pickerButton.isHidden = true + } + } + + // MARK: - UIColorWell + + // Update the color view from the color well chosen action. + func colorWellHandler(action: UIAction) { + if let colorWell = action.sender as? UIColorWell { + colorView.backgroundColor = colorWell.selectedColor + } + } + + func configureColorWell() { + /** Note: Both color well and picker buttons achieve the same thing, presenting the color picker. + But one presents it with a color well control, the other by a bar button item. + */ + let colorWellAction = UIAction(title: "", handler: colorWellHandler) + colorWell = + UIColorWell(frame: CGRect(x: 0, y: 0, width: 32, height: 32), primaryAction: colorWellAction) + + // For Mac Catalyst, the UIColorWell is placed in the main view. + if navigationController?.traitCollection.userInterfaceIdiom == .mac { + pickerWellView.addSubview(colorWell) + } else { + // For iOS, the UIColorWell is placed inside the navigation bar as a UIBarButtonItem. + let colorWellBarItem = UIBarButtonItem(customView: colorWell) + let fixedBarItem = UIBarButtonItem.fixedSpace(20.0) + navigationItem.rightBarButtonItems!.append(fixedBarItem) + navigationItem.rightBarButtonItems!.append(colorWellBarItem) + } + } + + // MARK: - UIColorPickerViewController + + func configureColorPicker() { + colorPicker = UIColorPickerViewController() + colorPicker.supportsAlpha = true + colorPicker.selectedColor = UIColor.blue + colorPicker.delegate = self + } + + // Present the color picker from the UIBarButtonItem, iOS only. + // This will present it as a popover (preferred), or for compact mode as a modal sheet. + @IBAction func presentColorPickerByBarButton(_ sender: UIBarButtonItem) { + colorPicker.modalPresentationStyle = UIModalPresentationStyle.popover // will display as popover for iPad or sheet for compact screens. + let popover: UIPopoverPresentationController = colorPicker.popoverPresentationController! + popover.barButtonItem = sender + present(colorPicker, animated: true, completion: nil) + } + + // Present the color picker from the UIButton, Mac Catalyst only. + // This will present it as a popover (preferred), or for compact mode as a modal sheet. + @IBAction func presentColorPickerByButton(_ sender: UIButton) { + colorPicker.modalPresentationStyle = UIModalPresentationStyle.popover + if let popover = colorPicker.popoverPresentationController { + popover.sourceView = sender + present(colorPicker, animated: true, completion: nil) + } + } + + // MARK: - UIColorPickerViewControllerDelegate + + // Color returned from the color picker via UIBarButtonItem - iOS 15.0 + @available(iOS 15.0, *) + func colorPickerViewController(_ viewController: UIColorPickerViewController, didSelect _: UIColor, continuously: Bool) { + // User has chosen a color. + let chosenColor = viewController.selectedColor + colorView.backgroundColor = chosenColor + + // Dismiss the color picker if the conditions are right: + // 1) User is not doing a continous pick (tap and drag across multiple colors). + // 2) Picker is presented on a non-compact device. + // + // Use the following check to determine how the color picker was presented (modal or popover). + // For popover, we want to dismiss it when a color is locked. + // For modal, the picker has a close button. + // + if !continuously { + if traitCollection.horizontalSizeClass != .compact { + viewController.dismiss(animated: true, completion: { + Swift.debugPrint("\(chosenColor)") + }) + } + } + } + + // Color returned from the color picker - iOS 14.x and earlier. + func colorPickerViewControllerDidSelectColor(_ viewController: UIColorPickerViewController) { + // User has chosen a color. + let chosenColor = viewController.selectedColor + colorView.backgroundColor = chosenColor + + // Use the following check to determine how the color picker was presented (modal or popover). + // For popover, we want to dismiss it when a color is locked. + // For modal, the picker has a close button. + // + if traitCollection.horizontalSizeClass != .compact { + viewController.dismiss(animated: true, completion: { + Swift.debugPrint("\(chosenColor)") + }) + } + } + + func colorPickerViewControllerDidFinish(_: UIColorPickerViewController) { + /** In presentations (except popovers) the color picker shows a close button. If the close button is tapped, + the view controller is dismissed and `colorPickerViewControllerDidFinish:` is called. Can be used to + animate alongside the dismissal. + */ + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/CustomPageControlViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/CustomPageControlViewController.swift new file mode 100755 index 000000000..e4b51028f --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/CustomPageControlViewController.swift @@ -0,0 +1,92 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A view controller that demonstrates how to use a customized `UIPageControl`. + */ + +import UIKit + +class CustomPageControlViewController: UIViewController { + // MARK: - Properties + + @IBOutlet var pageControl: UIPageControl! + + @IBOutlet var colorView: UIView! + + // Colors that correspond to the selected page. Used as the background color for `colorView`. + let colors = [ + UIColor.black, + UIColor.systemGray, + UIColor.systemRed, + UIColor.systemGreen, + UIColor.systemBlue, + UIColor.systemPink, + UIColor.systemYellow, + UIColor.systemIndigo, + UIColor.systemOrange, + UIColor.systemPurple, + UIColor.systemGray2, + UIColor.systemGray3, + UIColor.systemGray4, + UIColor.systemGray5, + ] + + let images = [ + UIImage(systemName: "square.fill"), + UIImage(systemName: "square"), + UIImage(systemName: "triangle.fill"), + UIImage(systemName: "triangle"), + UIImage(systemName: "circle.fill"), + UIImage(systemName: "circle"), + UIImage(systemName: "star.fill"), + UIImage(systemName: "star"), + UIImage(systemName: "staroflife"), + UIImage(systemName: "staroflife.fill"), + UIImage(systemName: "heart.fill"), + UIImage(systemName: "heart"), + UIImage(systemName: "moon"), + UIImage(systemName: "moon.fill"), + ] + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + configurePageControl() + pageControlValueDidChange() + } + + // MARK: - Configuration + + func configurePageControl() { + // The total number of available pages is based on the number of available colors. + pageControl.numberOfPages = colors.count + pageControl.currentPage = 2 + + pageControl.currentPageIndicatorTintColor = UIColor.systemPurple + + // Prominent background style. + pageControl.backgroundStyle = .prominent + + // Set custom indicator images. + for (index, image) in images.enumerated() { + pageControl.setIndicatorImage(image, forPage: index) + } + + pageControl.addTarget(self, + action: #selector(PageControlViewController.pageControlValueDidChange), + for: .valueChanged) + } + + // MARK: - Actions + + @objc + func pageControlValueDidChange() { + // Note: gesture swiping between pages is provided by `UIPageViewController` and not `UIPageControl`. + Swift.debugPrint("The page control changed its current page to \(pageControl.currentPage).") + + colorView.backgroundColor = colors[pageControl.currentPage] + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/CustomSearchBarViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/CustomSearchBarViewController.swift new file mode 100755 index 000000000..c8dc65f03 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/CustomSearchBarViewController.swift @@ -0,0 +1,60 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A view controller that demonstrates how to customize a `UISearchBar`. + */ + +import UIKit + +class CustomSearchBarViewController: UIViewController { + // MARK: - Properties + + @IBOutlet var searchBar: UISearchBar! + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + configureSearchBar() + } + + // MARK: - Configuration + + func configureSearchBar() { + searchBar.showsCancelButton = true + searchBar.showsBookmarkButton = true + + searchBar.tintColor = UIColor.systemPurple + + searchBar.backgroundImage = UIImage(named: "search_bar_background") + + // Set the bookmark image for both normal and highlighted states. + let bookImage = UIImage(systemName: "bookmark") + searchBar.setImage(bookImage, for: .bookmark, state: .normal) + + let bookFillImage = UIImage(systemName: "bookmark.fill") + searchBar.setImage(bookFillImage, for: .bookmark, state: .highlighted) + } +} + +// MARK: - UISearchBarDelegate + +extension CustomSearchBarViewController: UISearchBarDelegate { + func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { + Swift.debugPrint("The custom search bar keyboard \"Search\" button was tapped.") + + searchBar.resignFirstResponder() + } + + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + Swift.debugPrint("The custom search bar \"Cancel\" button was tapped.") + + searchBar.resignFirstResponder() + } + + func searchBarBookmarkButtonClicked(_: UISearchBar) { + Swift.debugPrint("The custom \"bookmark button\" inside the search bar was tapped.") + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/CustomToolbarViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/CustomToolbarViewController.swift new file mode 100755 index 000000000..d9a96899a --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/CustomToolbarViewController.swift @@ -0,0 +1,71 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A view controller that demonstrates how to customize a `UIToolbar`. + */ + +import UIKit + +class CustomToolbarViewController: UIViewController { + // MARK: - Properties + + @IBOutlet var toolbar: UIToolbar! + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + let toolbarBackgroundImage = UIImage(named: "toolbar_background") + toolbar.setBackgroundImage(toolbarBackgroundImage, forToolbarPosition: .bottom, barMetrics: .default) + + let toolbarButtonItems = [ + customImageBarButtonItem, + flexibleSpaceBarButtonItem, + customBarButtonItem, + ] + toolbar.setItems(toolbarButtonItems, animated: true) + } + + // MARK: - UIBarButtonItem Creation and Configuration + + var customImageBarButtonItem: UIBarButtonItem { + let customBarButtonItemImage = UIImage(systemName: "exclamationmark.triangle") + + let customImageBarButtonItem = UIBarButtonItem(image: customBarButtonItemImage, + style: .plain, + target: self, + action: #selector(CustomToolbarViewController.barButtonItemClicked(_:))) + + customImageBarButtonItem.tintColor = UIColor.systemPurple + + return customImageBarButtonItem + } + + var flexibleSpaceBarButtonItem: UIBarButtonItem { + // Note that there's no target/action since this represents empty space. + UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + } + + var customBarButtonItem: UIBarButtonItem { + let barButtonItem = UIBarButtonItem(title: NSLocalizedString("Button", comment: ""), + style: .plain, + target: self, + action: #selector(CustomToolbarViewController.barButtonItemClicked)) + + let attributes = [ + NSAttributedString.Key.foregroundColor: UIColor.systemPurple, + ] + barButtonItem.setTitleTextAttributes(attributes, for: []) + + return barButtonItem + } + + // MARK: - Actions + + @objc + func barButtonItemClicked(_ barButtonItem: UIBarButtonItem) { + Swift.debugPrint("A bar button item on the custom toolbar was clicked: \(barButtonItem).") + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/DatePickerController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/DatePickerController.swift new file mode 100755 index 000000000..0a0729a69 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/DatePickerController.swift @@ -0,0 +1,82 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A view controller that demonstrates how to use `UIDatePicker`. + */ + +import UIKit + +class DatePickerController: UIViewController { + // MARK: - Properties + + @IBOutlet var datePicker: UIDatePicker! + + @IBOutlet var dateLabel: UILabel! + + // A date formatter to format the `date` property of `datePicker`. + lazy var dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .short + + return dateFormatter + }() + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + if #available(iOS 15, *) { + // In case the label's content is too large to fit inside the label (causing truncation), + // use this to reveal the label's full text drawn as a tool tip. + dateLabel.showsExpansionTextWhenTruncated = true + } + + configureDatePicker() + } + + // MARK: - Configuration + + func configureDatePicker() { + datePicker.datePickerMode = .dateAndTime + + /** Set min/max date for the date picker. As an example we will limit the date between + now and 7 days from now. + */ + let now = Date() + datePicker.minimumDate = now + + // Decide the best date picker style based on the trait collection's vertical size. + datePicker.preferredDatePickerStyle = traitCollection.verticalSizeClass == .compact ? .compact : .inline + + var dateComponents = DateComponents() + dateComponents.day = 7 + + let sevenDaysFromNow = Calendar.current.date(byAdding: .day, value: 7, to: now) + datePicker.maximumDate = sevenDaysFromNow + + datePicker.minuteInterval = 2 + + datePicker.addTarget(self, action: #selector(DatePickerController.updateDatePickerLabel), for: .valueChanged) + + updateDatePickerLabel() + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + // Adjust the date picker style due to the trait collection's vertical size. + super.traitCollectionDidChange(previousTraitCollection) + datePicker.preferredDatePickerStyle = traitCollection.verticalSizeClass == .compact ? .compact : .inline + } + + // MARK: - Actions + + @objc + func updateDatePickerLabel() { + dateLabel.text = dateFormatter.string(from: datePicker.date) + + Swift.debugPrint("Chosen date: \(dateFormatter.string(from: datePicker.date))") + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/DefaultPageControlViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/DefaultPageControlViewController.swift new file mode 100755 index 000000000..df9ed56b8 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/DefaultPageControlViewController.swift @@ -0,0 +1,62 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A view controller that demonstrates how to use `UIPageControl`. + */ + +import UIKit + +class PageControlViewController: UIViewController { + // MARK: - Properties + + @IBOutlet var pageControl: UIPageControl! + + @IBOutlet var colorView: UIView! + + // Colors that correspond to the selected page. Used as the background color for `colorView`. + let colors = [ + UIColor.black, + UIColor.systemGray, + UIColor.systemRed, + UIColor.systemGreen, + UIColor.systemBlue, + UIColor.systemPink, + UIColor.systemYellow, + UIColor.systemIndigo, + UIColor.systemOrange, + UIColor.systemPurple, + ] + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + configurePageControl() + pageControlValueDidChange() + } + + // MARK: - Configuration + + func configurePageControl() { + // The total number of available pages is based on the number of available colors. + pageControl.numberOfPages = colors.count + pageControl.currentPage = 2 + + pageControl.pageIndicatorTintColor = UIColor.systemGreen + pageControl.currentPageIndicatorTintColor = UIColor.systemPurple + + pageControl.addTarget(self, action: #selector(PageControlViewController.pageControlValueDidChange), for: .valueChanged) + } + + // MARK: - Actions + + @objc + func pageControlValueDidChange() { + // Note: gesture swiping between pages is provided by `UIPageViewController` and not `UIPageControl`. + Swift.debugPrint("The page control changed its current page to \(pageControl.currentPage).") + + colorView.backgroundColor = colors[pageControl.currentPage] + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/DefaultSearchBarViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/DefaultSearchBarViewController.swift new file mode 100755 index 000000000..1d8f7665c --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/DefaultSearchBarViewController.swift @@ -0,0 +1,54 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A view controller that demonstrates how to use a default `UISearchBar`. + */ + +import UIKit + +class DefaultSearchBarViewController: UIViewController { + // MARK: - Properties + + @IBOutlet var searchBar: UISearchBar! + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + configureSearchBar() + } + + // MARK: - Configuration + + func configureSearchBar() { + searchBar.showsCancelButton = true + searchBar.showsScopeBar = true + + searchBar.scopeButtonTitles = [ + NSLocalizedString("Scope One", comment: ""), + NSLocalizedString("Scope Two", comment: ""), + ] + } +} + +// MARK: - UISearchBarDelegate + +extension DefaultSearchBarViewController: UISearchBarDelegate { + func searchBar(_: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) { + Swift.debugPrint("The default search selected scope button index changed to \(selectedScope).") + } + + func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { + Swift.debugPrint("The default search bar keyboard search button was tapped: \(String(describing: searchBar.text)).") + + searchBar.resignFirstResponder() + } + + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + Swift.debugPrint("The default search bar cancel button was tapped.") + + searchBar.resignFirstResponder() + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/DefaultToolbarViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/DefaultToolbarViewController.swift new file mode 100755 index 000000000..5dbab54d4 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/DefaultToolbarViewController.swift @@ -0,0 +1,60 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A view controller that demonstrates how to use a default `UIToolbar`. + */ + +import UIKit + +class DefaultToolbarViewController: UIViewController { + // MARK: - Properties + + @IBOutlet var toolbar: UIToolbar! + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + let toolbarButtonItems = [ + trashBarButtonItem, + flexibleSpaceBarButtonItem, + customTitleBarButtonItem, + ] + toolbar.setItems(toolbarButtonItems, animated: true) + } + + // MARK: - UIBarButtonItem Creation and Configuration + + var trashBarButtonItem: UIBarButtonItem { + UIBarButtonItem(barButtonSystemItem: .trash, + target: self, + action: #selector(DefaultToolbarViewController.barButtonItemClicked(_:))) + } + + var flexibleSpaceBarButtonItem: UIBarButtonItem { + UIBarButtonItem(barButtonSystemItem: .flexibleSpace, + target: nil, + action: nil) + } + + func menuHandler(action: UIAction) { + Swift.debugPrint("Menu Action '\(action.title)'") + } + + var customTitleBarButtonItem: UIBarButtonItem { + let buttonMenu = UIMenu(title: "", + children: (1 ... 5).map { + UIAction(title: "Option \($0)", handler: menuHandler) + }) + return UIBarButtonItem(image: UIImage(systemName: "list.number"), menu: buttonMenu) + } + + // MARK: - Actions + + @objc + func barButtonItemClicked(_ barButtonItem: UIBarButtonItem) { + Swift.debugPrint("A bar button item on the default toolbar was clicked: \(barButtonItem).") + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/FontPickerViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/FontPickerViewController.swift new file mode 100755 index 000000000..b6e71e123 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/FontPickerViewController.swift @@ -0,0 +1,103 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A view controller that demonstrates how to use `UIFontPickerViewController`. + */ + +import UIKit + +class FontPickerViewController: UIViewController { + // MARK: - Properties + + var fontPicker: UIFontPickerViewController! + var textFormatter: UITextFormattingCoordinator! + + @IBOutlet var fontLabel: UILabel! + @IBOutlet var textFormatterButton: UIButton! + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + fontLabel.text = NSLocalizedString("SampleFontTitle", comment: "") + + configureFontPicker() + + if traitCollection.userInterfaceIdiom != .mac { + // UITextFormattingCoordinator's toggleFontPanel is available only for macOS. + textFormatterButton.isHidden = true + } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + configureTextFormatter() + } + + func configureFontPicker() { + let configuration = UIFontPickerViewController.Configuration() + configuration.includeFaces = true + configuration.displayUsingSystemFont = false + configuration.filteredTraits = [.classModernSerifs] + + fontPicker = UIFontPickerViewController(configuration: configuration) + fontPicker.delegate = self + fontPicker.modalPresentationStyle = UIModalPresentationStyle.popover + } + + func configureTextFormatter() { + if textFormatter == nil { + guard let scene = view.window?.windowScene else { return } + let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: fontLabel.font as Any] + textFormatter = UITextFormattingCoordinator(for: scene) + textFormatter.delegate = self + textFormatter.setSelectedAttributes(attributes, isMultiple: true) + } + } + + @IBAction func presentFontPicker(_ sender: Any) { + if let button = sender as? UIButton { + let popover: UIPopoverPresentationController = fontPicker.popoverPresentationController! + popover.sourceView = button + present(fontPicker, animated: true, completion: nil) + } + } + + @IBAction func presentTextFormattingCoordinator(_ sender: Any) { + if !UITextFormattingCoordinator.isFontPanelVisible { + UITextFormattingCoordinator.toggleFontPanel(sender) + } + } +} + +// MARK: - UIFontPickerViewControllerDelegate + +extension FontPickerViewController: UIFontPickerViewControllerDelegate { + func fontPickerViewControllerDidCancel(_: UIFontPickerViewController) { + // .. + } + + func fontPickerViewControllerDidPickFont(_ viewController: UIFontPickerViewController) { + guard let fontDescriptor = viewController.selectedFontDescriptor else { return } + let font = UIFont(descriptor: fontDescriptor, size: 28.0) + fontLabel.font = font + } +} + +// MARK: - UITextFormattingCoordinatorDelegate + +extension FontPickerViewController: UITextFormattingCoordinatorDelegate { + override func updateTextAttributes(conversionHandler: ([NSAttributedString.Key: Any]) -> [NSAttributedString.Key: Any]) { + guard let oldLabelText = fontLabel.attributedText else { return } + let newString = NSMutableAttributedString(string: oldLabelText.string) + oldLabelText.enumerateAttributes(in: NSRange(location: 0, length: oldLabelText.length), + options: []) + { attributeDictionary, range, _ in + newString.setAttributes(conversionHandler(attributeDictionary), range: range) + } + fontLabel.attributedText = newString + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/ImagePickerViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/ImagePickerViewController.swift new file mode 100755 index 000000000..4d4c3619b --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/ImagePickerViewController.swift @@ -0,0 +1,45 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A view controller that demonstrates how to use `UIFontPickerViewController`. + */ + +import UIKit + +class ImagePickerViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate { + // MARK: - Properties + + var imagePicker: UIImagePickerController! + @IBOutlet var imageView: UIImageView! + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + configureImagePicker() + } + + func configureImagePicker() { + imagePicker = UIImagePickerController() + imagePicker.delegate = self + imagePicker.mediaTypes = ["public.image"] + imagePicker.sourceType = .photoLibrary + } + + @IBAction func presentImagePicker(_: AnyObject) { + present(imagePicker, animated: true) + } + + // MARK: - UIImagePickerControllerDelegate + + func imagePickerController(_ picker: UIImagePickerController, + didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) + { + if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage { + imageView.image = image + } + picker.dismiss(animated: true, completion: nil) + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/ImageViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/ImageViewController.swift new file mode 100755 index 000000000..8641f2e48 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/ImageViewController.swift @@ -0,0 +1,44 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A view controller that demonstrates how to use `UIImageView`. + */ + +import UIKit + +class ImageViewController: UIViewController { + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + configureImageView() + } + + // MARK: - Configuration + + func configureImageView() { + // The root view of the view controller is set in Interface Builder and is an UIImageView. + if let imageView = view as? UIImageView { + // Fetch the images (each image is of the format Flowers_number). + imageView.animationImages = (1 ... 2).map { UIImage(named: "Flowers_\($0)")! } + + // We want the image to be scaled to the correct aspect ratio within imageView's bounds. + imageView.contentMode = .scaleAspectFit + + imageView.animationDuration = 5 + imageView.startAnimating() + + imageView.isAccessibilityElement = true + imageView.accessibilityLabel = NSLocalizedString("Animated", comment: "") + + if #available(iOS 15, *) { + // This case uses UIToolTipInteraction which is available on iOS 15 or later. + let interaction = + UIToolTipInteraction(defaultToolTip: NSLocalizedString("ImageToolTipTitle", comment: "")) + imageView.addInteraction(interaction) + } + } + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/Info.plist b/PostHogExampleAutocapture/PostHogExampleAutocapture/Info.plist new file mode 100755 index 000000000..bb77aaa23 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/Info.plist @@ -0,0 +1,70 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UILaunchStoryboardName + LaunchScreen + UISceneClassName + UIWindowScene + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/MenuButtonViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/MenuButtonViewController.swift new file mode 100755 index 000000000..fe9e5413c --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/MenuButtonViewController.swift @@ -0,0 +1,185 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A view controller that demonstrates how to attach menus to `UIButton`. + */ + +import UIKit + +class MenuButtonViewController: BaseTableViewController { + // Cell identifier for each menu button table view cell. + enum MenuButtonKind: String, CaseIterable { + case buttonMenuProgrammatic + case buttonMenuMultiAction + case buttonSubMenu + case buttonMenuSelection + } + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("DropDownProgTitle", comment: ""), + cellID: MenuButtonKind.buttonMenuProgrammatic.rawValue, + configHandler: configureDropDownProgrammaticButton), + CaseElement(title: NSLocalizedString("DropDownMultiActionTitle", comment: ""), + cellID: MenuButtonKind.buttonMenuMultiAction.rawValue, + configHandler: configureDropdownMultiActionButton), + CaseElement(title: NSLocalizedString("DropDownButtonSubMenuTitle", comment: ""), + cellID: MenuButtonKind.buttonSubMenu.rawValue, + configHandler: configureDropdownSubMenuButton), + CaseElement(title: NSLocalizedString("PopupSelection", comment: ""), + cellID: MenuButtonKind.buttonMenuSelection.rawValue, + configHandler: configureSelectionPopupButton), + ]) + } + + // MARK: - Handlers + + enum ButtonMenuActionIdentifiers: String { + case item1 + case item2 + case item3 + } + + func menuHandler(action: UIAction) { + switch action.identifier.rawValue { + case ButtonMenuActionIdentifiers.item1.rawValue: + Swift.debugPrint("Menu Action: item 1") + case ButtonMenuActionIdentifiers.item2.rawValue: + Swift.debugPrint("Menu Action: item 2") + case ButtonMenuActionIdentifiers.item3.rawValue: + Swift.debugPrint("Menu Action: item 3") + default: break + } + } + + func item4Handler(action: UIAction) { + Swift.debugPrint("Menu Action: \(action.title)") + } + + // MARK: - Drop Down Menu Buttons + + func configureDropDownProgrammaticButton(button: UIButton) { + button.menu = UIMenu(children: [ + UIAction(title: String(format: NSLocalizedString("ItemTitle", comment: ""), "1"), + identifier: UIAction.Identifier(ButtonMenuActionIdentifiers.item1.rawValue), + handler: menuHandler), + UIAction(title: String(format: NSLocalizedString("ItemTitle", comment: ""), "2"), + identifier: UIAction.Identifier(ButtonMenuActionIdentifiers.item2.rawValue), + handler: menuHandler), + ]) + + button.showsMenuAsPrimaryAction = true + } + + func configureDropdownMultiActionButton(button: UIButton) { + let buttonMenu = UIMenu(children: [ + // Share a single handler for the first 3 actions. + UIAction(title: String(format: NSLocalizedString("ItemTitle", comment: ""), "1"), + image: UIImage(systemName: "1.circle"), + identifier: UIAction.Identifier(ButtonMenuActionIdentifiers.item1.rawValue), + attributes: [], + handler: menuHandler), + UIAction(title: String(format: NSLocalizedString("ItemTitle", comment: ""), "2"), + image: UIImage(systemName: "2.circle"), + identifier: UIAction.Identifier(ButtonMenuActionIdentifiers.item2.rawValue), + handler: menuHandler), + UIAction(title: String(format: NSLocalizedString("ItemTitle", comment: ""), "3"), + image: UIImage(systemName: "3.circle"), + identifier: UIAction.Identifier(ButtonMenuActionIdentifiers.item3.rawValue), + handler: menuHandler), + + // Use a separate handler for this 4th action. + UIAction(title: String(format: NSLocalizedString("ItemTitle", comment: ""), "4"), + image: UIImage(systemName: "4.circle"), + identifier: nil, + handler: item4Handler(action:)), + + // Use a closure for the 5th action. + UIAction(title: String(format: NSLocalizedString("ItemTitle", comment: ""), "5"), + image: UIImage(systemName: "5.circle"), + identifier: nil) + { action in + Swift.debugPrint("Menu Action: \(action.title)") + }, + + // Use attributes to make the 6th action disabled. + UIAction(title: String(format: NSLocalizedString("ItemTitle", comment: ""), "6"), + image: UIImage(systemName: "6.circle"), + identifier: nil, + attributes: [UIMenuElement.Attributes.disabled]) + { action in + Swift.debugPrint("Menu Action: \(action.title)") + }, + ]) + button.menu = buttonMenu + + // This makes the button behave like a drop down menu. + button.showsMenuAsPrimaryAction = true + } + + func configureDropdownSubMenuButton(button: UIButton) { + let sortClosure = { (action: UIAction) in + Swift.debugPrint("Sort by: \(action.title)") + } + let refreshClosure = { (_: UIAction) in + Swift.debugPrint("Refresh handler") + } + let accountHandler = { (_: UIAction) in + Swift.debugPrint("Account handler") + } + + var sortMenu: UIMenu + if #available(iOS 15, *) { // .singleSelection option only on iOS 15 or later + // The sort sub menu supports a selection. + sortMenu = UIMenu(title: "Sort By", options: .singleSelection, children: [ + UIAction(title: "Date", state: .on, handler: sortClosure), + UIAction(title: "Size", handler: sortClosure), + ]) + } else { + sortMenu = UIMenu(title: "Sort By", children: [ + UIAction(title: "Date", handler: sortClosure), + UIAction(title: "Size", handler: sortClosure), + ]) + } + + let topMenu = UIMenu(children: [ + UIAction(title: "Refresh", handler: refreshClosure), + UIAction(title: "Account", handler: accountHandler), + sortMenu, + ]) + + // This makes the button behave like a drop down menu. + button.showsMenuAsPrimaryAction = true + button.menu = topMenu + } + + // MARK: - Selection Popup Menu Button + + func updateColor(_ title: String) { + Swift.debugPrint("Color selected: \(title)") + } + + func configureSelectionPopupButton(button: UIButton) { + let colorClosure = { [unowned self] (action: UIAction) in + updateColor(action.title) + } + + button.menu = UIMenu(children: [ + UIAction(title: "Red", handler: colorClosure), + UIAction(title: "Green", state: .on, handler: colorClosure), // The default selected item (green). + UIAction(title: "Blue", handler: colorClosure), + ]) + + // This makes the button behave like a drop down menu. + button.showsMenuAsPrimaryAction = true + + if #available(iOS 15, *) { + button.changesSelectionAsPrimaryAction = true + // Select the default menu item (green). + updateColor((button.menu?.selectedElements.first!.title)!) + } + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/OutlineViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/OutlineViewController.swift new file mode 100755 index 000000000..04be62c1a --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/OutlineViewController.swift @@ -0,0 +1,323 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A simple outline view for the sample app's main UI + */ + +import UIKit + +class OutlineViewController: UIViewController { + enum Section { + case main + } + + class OutlineItem: Identifiable, Hashable { + let title: String + let subitems: [OutlineItem] + let storyboardName: String? + let imageName: String? + + init(title: String, imageName: String?, storyboardName: String? = nil, subitems: [OutlineItem] = []) { + self.title = title + self.subitems = subitems + self.storyboardName = storyboardName + self.imageName = imageName + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + static func == (lhs: OutlineItem, rhs: OutlineItem) -> Bool { + lhs.id == rhs.id + } + } + + var dataSource: UICollectionViewDiffableDataSource! = nil + var outlineCollectionView: UICollectionView! = nil + + private var detailTargetChangeObserver: Any? = nil + + override func viewDidLoad() { + super.viewDidLoad() + + configureCollectionView() + configureDataSource() + + // Add a translucent background to the primary view controller for the Mac. + splitViewController!.primaryBackgroundStyle = .sidebar + view.backgroundColor = UIColor.clear + + // Listen for when the split view controller is expanded or collapsed for iPad multi-tasking, + // and on device rotate (iPhones that support regular size class). + detailTargetChangeObserver = + NotificationCenter.default.addObserver(forName: UIViewController.showDetailTargetDidChangeNotification, + object: nil, + queue: OperationQueue.main, + using: { _ in + // Posted when a split view controller is expanded or collapsed. + + // Re-load the data source, the disclosure indicators need to change (push vs. present on a cell). + var snapshot = self.dataSource.snapshot() + snapshot.reloadItems(self.menuItems) + self.dataSource.apply(snapshot, animatingDifferences: false) + }) + + if navigationController!.traitCollection.userInterfaceIdiom == .mac { + navigationController!.navigationBar.isHidden = true + } + } + + deinit { + if let observer = detailTargetChangeObserver { + NotificationCenter.default.removeObserver(observer) + } + } + + lazy var controlsOutlineItem: OutlineItem = { + // Determine the content of the UIButton grouping. + var buttonItems = [ + OutlineItem(title: NSLocalizedString("ButtonsTitle", comment: ""), imageName: "rectangle", + storyboardName: "ButtonViewController"), + OutlineItem(title: NSLocalizedString("MenuButtonsTitle", comment: ""), imageName: "list.bullet.rectangle", + storyboardName: "MenuButtonViewController"), + ] + // UIPointerInteraction to UIButtons is applied for iPad. + if navigationController!.traitCollection.userInterfaceIdiom == .pad { + buttonItems.append(contentsOf: + [OutlineItem(title: NSLocalizedString("PointerInteractionButtonsTitle", comment: ""), + imageName: "cursorarrow.rays", + storyboardName: "PointerInteractionButtonViewController")]) + } + + var controlsSubItems = [ + OutlineItem(title: NSLocalizedString("ButtonsTitle", comment: ""), imageName: "rectangle.on.rectangle", subitems: buttonItems), + + OutlineItem(title: NSLocalizedString("PageControlTitle", comment: ""), imageName: "photo.on.rectangle", subitems: [ + OutlineItem(title: NSLocalizedString("DefaultPageControlTitle", comment: ""), imageName: nil, + storyboardName: "DefaultPageControlViewController"), + OutlineItem(title: NSLocalizedString("CustomPageControlTitle", comment: ""), imageName: nil, + storyboardName: "CustomPageControlViewController"), + ]), + + OutlineItem(title: NSLocalizedString("SearchBarsTitle", comment: ""), imageName: "magnifyingglass", subitems: [ + OutlineItem(title: NSLocalizedString("DefaultSearchBarTitle", comment: ""), imageName: nil, + storyboardName: "DefaultSearchBarViewController"), + OutlineItem(title: NSLocalizedString("CustomSearchBarTitle", comment: ""), imageName: nil, + storyboardName: "CustomSearchBarViewController"), + ]), + + OutlineItem(title: NSLocalizedString("SegmentedControlsTitle", comment: ""), imageName: "square.split.3x1", + storyboardName: "SegmentedControlViewController"), + OutlineItem(title: NSLocalizedString("SlidersTitle", comment: ""), imageName: nil, + storyboardName: "SliderViewController"), + OutlineItem(title: NSLocalizedString("SwitchesTitle", comment: ""), imageName: nil, + storyboardName: "SwitchViewController"), + OutlineItem(title: NSLocalizedString("TextFieldsTitle", comment: ""), imageName: nil, + storyboardName: "TextFieldViewController"), + ] + + if traitCollection.userInterfaceIdiom != .mac { + // UIStepper class is not supported when running Mac Catalyst apps in the Mac idiom. + let stepperItem = + OutlineItem(title: NSLocalizedString("SteppersTitle", comment: ""), imageName: nil, storyboardName: "StepperViewController") + controlsSubItems.append(stepperItem) + } + + return OutlineItem(title: "Controls", imageName: "slider.horizontal.3", subitems: controlsSubItems) + }() + + lazy var pickersOutlineItem: OutlineItem = { + var pickerSubItems = [ + OutlineItem(title: NSLocalizedString("DatePickerTitle", comment: ""), imageName: nil, + storyboardName: "DatePickerController"), + OutlineItem(title: NSLocalizedString("ColorPickerTitle", comment: ""), imageName: nil, + storyboardName: "ColorPickerViewController"), + OutlineItem(title: NSLocalizedString("FontPickerTitle", comment: ""), imageName: nil, + storyboardName: "FontPickerViewController"), + OutlineItem(title: NSLocalizedString("ImagePickerTitle", comment: ""), imageName: nil, + storyboardName: "ImagePickerViewController"), + ] + + if traitCollection.userInterfaceIdiom != .mac { + // UIPickerView class is not supported when running Mac Catalyst apps in the Mac idiom. + // To use a picker in macOS, use UIButton with changesSelectionAsPrimaryAction set to "true". + let pickerViewItem = + OutlineItem(title: NSLocalizedString("PickerViewTitle", comment: ""), imageName: nil, storyboardName: "PickerViewController") + pickerSubItems.append(pickerViewItem) + } + + return OutlineItem(title: "Pickers", imageName: "list.bullet", subitems: pickerSubItems) + }() + + lazy var viewsOutlineItem: OutlineItem = .init(title: "Views", imageName: "rectangle.stack.person.crop", subitems: [ + OutlineItem(title: NSLocalizedString("ActivityIndicatorsTitle", comment: ""), imageName: nil, + storyboardName: "ActivityIndicatorViewController"), + OutlineItem(title: NSLocalizedString("AlertControllersTitle", comment: ""), imageName: nil, + storyboardName: "AlertControllerViewController"), + OutlineItem(title: NSLocalizedString("TextViewTitle", comment: ""), imageName: nil, + storyboardName: "TextViewController"), + + OutlineItem(title: NSLocalizedString("ImagesTitle", comment: ""), imageName: "photo", subitems: [ + OutlineItem(title: NSLocalizedString("ImageViewTitle", comment: ""), imageName: nil, + storyboardName: "ImageViewController"), + OutlineItem(title: NSLocalizedString("SymbolsTitle", comment: ""), imageName: nil, + storyboardName: "SymbolViewController"), + ]), + + OutlineItem(title: NSLocalizedString("ProgressViewsTitle", comment: ""), imageName: nil, + storyboardName: "ProgressViewController"), + OutlineItem(title: NSLocalizedString("StackViewsTitle", comment: ""), imageName: nil, + storyboardName: "StackViewController"), + + OutlineItem(title: NSLocalizedString("ToolbarsTitle", comment: ""), imageName: "hammer", subitems: [ + OutlineItem(title: NSLocalizedString("DefaultToolBarTitle", comment: ""), imageName: nil, + storyboardName: "DefaultToolbarViewController"), + OutlineItem(title: NSLocalizedString("TintedToolbarTitle", comment: ""), imageName: nil, + storyboardName: "TintedToolbarViewController"), + OutlineItem(title: NSLocalizedString("CustomToolbarBarTitle", comment: ""), imageName: nil, + storyboardName: "CustomToolbarViewController"), + ]), + + OutlineItem(title: NSLocalizedString("VisualEffectTitle", comment: ""), imageName: nil, storyboardName: "VisualEffectViewController"), + + OutlineItem(title: NSLocalizedString("WebViewTitle", comment: ""), imageName: nil, storyboardName: "WebViewController"), + ]) + + private lazy var menuItems: [OutlineItem] = [ + controlsOutlineItem, + viewsOutlineItem, + pickersOutlineItem, + ] +} + +// MARK: - UICollectionViewDiffableDataSource + +extension OutlineViewController { + private func configureCollectionView() { + let collectionView = + UICollectionView(frame: view.bounds, collectionViewLayout: generateLayout()) + view.addSubview(collectionView) + collectionView.autoresizingMask = [.flexibleHeight, .flexibleWidth] + outlineCollectionView = collectionView + collectionView.delegate = self + } + + private func configureDataSource() { + let containerCellRegistration = UICollectionView.CellRegistration { cell, _, menuItem in + + var contentConfiguration = cell.defaultContentConfiguration() + contentConfiguration.text = menuItem.title + + if let image = menuItem.imageName { + contentConfiguration.image = UIImage(systemName: image) + } + + contentConfiguration.textProperties.font = .preferredFont(forTextStyle: .headline) + cell.contentConfiguration = contentConfiguration + + let disclosureOptions = UICellAccessory.OutlineDisclosureOptions(style: .header) + cell.accessories = [.outlineDisclosure(options: disclosureOptions)] + + let background = UIBackgroundConfiguration.clear() + cell.backgroundConfiguration = background + } + + let cellRegistration = UICollectionView.CellRegistration { cell, _, menuItem in + var contentConfiguration = cell.defaultContentConfiguration() + contentConfiguration.text = menuItem.title + + if let image = menuItem.imageName { + contentConfiguration.image = UIImage(systemName: image) + } + + cell.contentConfiguration = contentConfiguration + + let background = UIBackgroundConfiguration.clear() + cell.backgroundConfiguration = background + + cell.accessories = self.splitViewWantsToShowDetail() ? [] : [.disclosureIndicator()] + } + + dataSource = UICollectionViewDiffableDataSource(collectionView: outlineCollectionView) { + (collectionView: UICollectionView, indexPath: IndexPath, item: OutlineItem) -> UICollectionViewCell? in + // Return the cell. + if item.subitems.isEmpty { + return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item) + } else { + return collectionView.dequeueConfiguredReusableCell(using: containerCellRegistration, for: indexPath, item: item) + } + } + + // Load our initial data. + let snapshot = initialSnapshot() + dataSource.apply(snapshot, to: .main, animatingDifferences: false) + } + + private func generateLayout() -> UICollectionViewLayout { + let listConfiguration = UICollectionLayoutListConfiguration(appearance: .sidebar) + let layout = UICollectionViewCompositionalLayout.list(using: listConfiguration) + return layout + } + + private func initialSnapshot() -> NSDiffableDataSourceSectionSnapshot { + var snapshot = NSDiffableDataSourceSectionSnapshot() + + func addItems(_ menuItems: [OutlineItem], to parent: OutlineItem?) { + snapshot.append(menuItems, to: parent) + for menuItem in menuItems where !menuItem.subitems.isEmpty { + addItems(menuItem.subitems, to: menuItem) + } + } + + addItems(menuItems, to: nil) + return snapshot + } +} + +// MARK: - UICollectionViewDelegate + +extension OutlineViewController: UICollectionViewDelegate { + private func splitViewWantsToShowDetail() -> Bool { + splitViewController?.traitCollection.horizontalSizeClass == .regular + } + + private func pushOrPresentViewController(viewController: UIViewController) { + if splitViewWantsToShowDetail() { + let navVC = UINavigationController(rootViewController: viewController) + splitViewController?.showDetailViewController(navVC, sender: navVC) // Replace the detail view controller. + + if navigationController!.traitCollection.userInterfaceIdiom == .mac { + navVC.navigationBar.isHidden = true + } + } else { + navigationController?.pushViewController(viewController, animated: true) // Just push instead of replace. + } + } + + private func pushOrPresentStoryboard(storyboardName: String) { + let exampleStoryboard = UIStoryboard(name: storyboardName, bundle: nil) + if let exampleViewController = exampleStoryboard.instantiateInitialViewController() { + pushOrPresentViewController(viewController: exampleViewController) + } + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard let menuItem = dataSource.itemIdentifier(for: indexPath) else { return } + + collectionView.deselectItem(at: indexPath, animated: true) + + if let storyboardName = menuItem.storyboardName { + pushOrPresentStoryboard(storyboardName: storyboardName) + + if navigationController!.traitCollection.userInterfaceIdiom == .mac { + if let windowScene = view.window?.windowScene { + if #available(iOS 15, *) { + windowScene.subtitle = menuItem.title + } + } + } + } + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/PickerViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/PickerViewController.swift new file mode 100755 index 000000000..27dba87f0 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/PickerViewController.swift @@ -0,0 +1,166 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A view controller that demonstrates how to use `UIPickerView`. + */ + +import UIKit + +class PickerViewController: UIViewController { + // MARK: - Types + + enum ColorComponent: Int { + case red = 0, green, blue + + static var count: Int { + ColorComponent.blue.rawValue + 1 + } + } + + enum RGB { + static let max: CGFloat = 255.0 + static let min: CGFloat = 0.0 + static let offset: CGFloat = 5.0 + } + + // MARK: - Properties + + @IBOutlet var pickerView: UIPickerView! + @IBOutlet var colorSwatchView: UIView! + + lazy var numberOfColorValuesPerComponent: Int = (Int(RGB.max) / Int(RGB.offset)) + 1 + + var redColor: CGFloat = RGB.min { + didSet { + updateColorSwatchViewBackgroundColor() + } + } + + var greenColor: CGFloat = RGB.min { + didSet { + updateColorSwatchViewBackgroundColor() + } + } + + var blueColor: CGFloat = RGB.min { + didSet { + updateColorSwatchViewBackgroundColor() + } + } + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + configurePickerView() + } + + func updateColorSwatchViewBackgroundColor() { + colorSwatchView.backgroundColor = UIColor(red: redColor, green: greenColor, blue: blueColor, alpha: 1) + } + + func configurePickerView() { + // Set the default selected rows (the desired rows to initially select will vary from app to app). + let selectedRows: [ColorComponent: Int] = [.red: 13, .green: 41, .blue: 24] + + for (colorComponent, selectedRow) in selectedRows { + /** Note that the delegate method on `UIPickerViewDelegate` is not triggered + when manually calling `selectRow(_:inComponent:animated:)`. To do + this, we fire off delegate method manually. + */ + pickerView.selectRow(selectedRow, inComponent: colorComponent.rawValue, animated: true) + pickerView(pickerView, didSelectRow: selectedRow, inComponent: colorComponent.rawValue) + } + } +} + +// MARK: - UIPickerViewDataSource + +extension PickerViewController: UIPickerViewDataSource { + func numberOfComponents(in _: UIPickerView) -> Int { + ColorComponent.count + } + + func pickerView(_: UIPickerView, numberOfRowsInComponent _: Int) -> Int { + numberOfColorValuesPerComponent + } +} + +// MARK: - UIPickerViewDelegate + +extension PickerViewController: UIPickerViewDelegate { + func pickerView(_: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { + let colorValue = CGFloat(row) * RGB.offset + + // Set the initial colors for each picker segment. + let value = CGFloat(colorValue) / RGB.max + var redColorComponent = RGB.min + var greenColorComponent = RGB.min + var blueColorComponent = RGB.min + + switch ColorComponent(rawValue: component)! { + case .red: + redColorComponent = value + + case .green: + greenColorComponent = value + + case .blue: + blueColorComponent = value + } + + if redColorComponent < 0.5 { + redColorComponent = 0.5 + } + if blueColorComponent < 0.5 { + blueColorComponent = 0.5 + } + if greenColorComponent < 0.5 { + greenColorComponent = 0.5 + } + let foregroundColor = UIColor(red: redColorComponent, green: greenColorComponent, blue: blueColorComponent, alpha: 1.0) + + // Set the foreground color for the entire attributed string. + let attributes = [ + NSAttributedString.Key.foregroundColor: foregroundColor, + ] + + let title = NSMutableAttributedString(string: "\(Int(colorValue))", attributes: attributes) + + return title + } + + func pickerView(_: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + let colorComponentValue = RGB.offset * CGFloat(row) / RGB.max + + switch ColorComponent(rawValue: component)! { + case .red: + redColor = colorComponentValue + + case .green: + greenColor = colorComponentValue + + case .blue: + blueColor = colorComponentValue + } + } +} + +// MARK: - UIPickerViewAccessibilityDelegate + +extension PickerViewController: UIPickerViewAccessibilityDelegate { + func pickerView(_: UIPickerView, accessibilityLabelForComponent component: Int) -> String? { + switch ColorComponent(rawValue: component)! { + case .red: + return NSLocalizedString("Red color component value", comment: "") + + case .green: + return NSLocalizedString("Green color component value", comment: "") + + case .blue: + return NSLocalizedString("Blue color component value", comment: "") + } + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/PointerInteractionButtonViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/PointerInteractionButtonViewController.swift new file mode 100755 index 000000000..80bc4828c --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/PointerInteractionButtonViewController.swift @@ -0,0 +1,167 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A view controller that demonstrates how to intergrate pointer interactions to `UIButton`. + */ + +import UIKit + +class PointerInteractionButtonViewController: BaseTableViewController { + // Cell identifier for each button pointer table view cell. + enum PointerButtonKind: String, CaseIterable { + case buttonPointer + case buttonHighlight + case buttonLift + case buttonHover + case buttonCustom + } + + // The pointer effect kind to use for each button (corresponds to the button's view tag). + enum ButtonPointerEffectKind: Int { + case pointer = 1 + case highlight + case lift + case hover + case custom + } + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: "UIPointerEffect.automatic", + cellID: PointerButtonKind.buttonPointer.rawValue, + configHandler: configurePointerButton), + CaseElement(title: "UIPointerEffect.highlight", + cellID: PointerButtonKind.buttonHighlight.rawValue, + configHandler: configureHighlightButton), + CaseElement(title: "UIPointerEffect.lift", + cellID: PointerButtonKind.buttonLift.rawValue, + configHandler: configureLiftButton), + CaseElement(title: "UIPointerEffect.hover", + cellID: PointerButtonKind.buttonHover.rawValue, + configHandler: configureHoverButton), + CaseElement(title: "UIPointerEffect (custom)", + cellID: PointerButtonKind.buttonCustom.rawValue, + configHandler: configureCustomButton), + ]) + } + + // MARK: - Configurations + + func configurePointerButton(button: UIButton) { + button.pointerStyleProvider = defaultButtonProvider + } + + func configureHighlightButton(button: UIButton) { + button.pointerStyleProvider = highlightButtonProvider + } + + func configureLiftButton(button: UIButton) { + button.pointerStyleProvider = liftButtonProvider + } + + func configureHoverButton(button: UIButton) { + button.pointerStyleProvider = hoverButtonProvider + } + + func configureCustomButton(button: UIButton) { + button.pointerStyleProvider = customButtonProvider + } + + // MARK: Button Pointer Providers + + func defaultButtonProvider(button _: UIButton, pointerEffect: UIPointerEffect, pointerShape: UIPointerShape) -> UIPointerStyle? { + var buttonPointerStyle: UIPointerStyle? = nil + + // Use the pointer effect's preview that's passed in. + let targetedPreview = pointerEffect.preview + + /** UIPointerEffect.automatic attempts to determine the appropriate effect for the given preview automatically. + The pointer effect has an automatic nature which adapts to the aspects of the button (background color, corner radius, size) + */ + let buttonPointerEffect = UIPointerEffect.automatic(targetedPreview) + buttonPointerStyle = UIPointerStyle(effect: buttonPointerEffect, shape: pointerShape) + return buttonPointerStyle + } + + func highlightButtonProvider(button _: UIButton, pointerEffect: UIPointerEffect, pointerShape: UIPointerShape) -> UIPointerStyle? { + var buttonPointerStyle: UIPointerStyle? = nil + + // Use the pointer effect's preview that's passed in. + let targetedPreview = pointerEffect.preview + + // Pointer slides under the given view and morphs into the view's shape. + let buttonHighlightPointerEffect = UIPointerEffect.highlight(targetedPreview) + buttonPointerStyle = UIPointerStyle(effect: buttonHighlightPointerEffect, shape: pointerShape) + + return buttonPointerStyle + } + + func liftButtonProvider(button: UIButton, pointerEffect: UIPointerEffect, pointerShape _: UIPointerShape) -> UIPointerStyle? { + var buttonPointerStyle: UIPointerStyle? = nil + + // Use the pointer effect's preview that's passed in. + let targetedPreview = pointerEffect.preview + + /** Pointer slides under the given view and disappears as the view scales up and gains a shadow. + Make the pointer shape’s bounds match the view’s frame so the highlight extends to the edges. + */ + let buttonLiftPointerEffect = UIPointerEffect.lift(targetedPreview) + let customPointerShape = UIPointerShape.path(UIBezierPath(roundedRect: button.bounds, cornerRadius: 6.0)) + buttonPointerStyle = UIPointerStyle(effect: buttonLiftPointerEffect, shape: customPointerShape) + + return buttonPointerStyle + } + + func hoverButtonProvider(button _: UIButton, pointerEffect: UIPointerEffect, pointerShape _: UIPointerShape) -> UIPointerStyle? { + var buttonPointerStyle: UIPointerStyle? = nil + + // Use the pointer effect's preview that's passed in. + let targetedPreview = pointerEffect.preview + + /** Pointer retains the system shape while over the given view. + Visual changes applied to the view are dictated by the effect's properties. + */ + let buttonHoverPointerEffect = + UIPointerEffect.hover(targetedPreview, preferredTintMode: .none, prefersShadow: true) + buttonPointerStyle = UIPointerStyle(effect: buttonHoverPointerEffect, shape: nil) + + return buttonPointerStyle + } + + func customButtonProvider(button: UIButton, pointerEffect _: UIPointerEffect, pointerShape _: UIPointerShape) -> UIPointerStyle? { + var buttonPointerStyle: UIPointerStyle? = nil + + /** Hover pointer with a custom triangle pointer shape. + Override the default UITargetedPreview with our own, make the visible path outset a little larger. + */ + let parameters = UIPreviewParameters() + parameters.visiblePath = UIBezierPath(rect: button.bounds.insetBy(dx: -15.0, dy: -15.0)) + let newTargetedPreview = UITargetedPreview(view: button, parameters: parameters) + + let buttonPointerEffect = + UIPointerEffect.hover(newTargetedPreview, preferredTintMode: .overlay, prefersShadow: false, prefersScaledContent: false) + + let customPointerShape = UIPointerShape.path(trianglePointerShape()) + buttonPointerStyle = UIPointerStyle(effect: buttonPointerEffect, shape: customPointerShape) + + return buttonPointerStyle + } + + // Return a triangle bezier path for the pointer's shape. + func trianglePointerShape() -> UIBezierPath { + let width = 20.0 + let height = 20.0 + let offset = 10.0 // Coordinate location to match up with the coordinate of default pointer shape. + + let pathView = UIBezierPath() + pathView.move(to: CGPoint(x: (width / 2) - offset, y: -offset)) + pathView.addLine(to: CGPoint(x: -offset, y: height - offset)) + pathView.addLine(to: CGPoint(x: width - offset, y: height - offset)) + pathView.close() + + return pathView + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/ProgressViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/ProgressViewController.swift new file mode 100755 index 000000000..6d833ad35 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/ProgressViewController.swift @@ -0,0 +1,131 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A view controller that demonstrates how to use `UIProgressView`. + */ + +import UIKit + +class ProgressViewController: BaseTableViewController { + // Cell identifier for each progress view table view cell. + enum ProgressViewKind: String, CaseIterable { + case defaultProgress + case barProgress + case tintedProgress + } + + // MARK: - Properties + + var observer: NSKeyValueObservation? + + // An `NSProgress` object whose `fractionCompleted` is observed using KVO to update the `UIProgressView`s' `progress` properties. + let progress = Progress(totalUnitCount: 10) + + // A repeating timer that, when fired, updates the `NSProgress` object's `completedUnitCount` property. + var updateTimer: Timer? + + var progressViews = [UIProgressView]() // Accumulated progress views from all table cells for progress updating. + + // MARK: - Initialization + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + + // Register as an observer of the `NSProgress`'s `fractionCompleted` property. + observer = progress.observe(\.fractionCompleted, options: [.new]) { _, _ in + // Update the progress views. + for progressView in self.progressViews { + progressView.setProgress(Float(self.progress.fractionCompleted), animated: true) + } + } + } + + deinit { + // Unregister as an observer of the `NSProgress`'s `fractionCompleted` property. + observer?.invalidate() + } + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("ProgressDefaultTitle", comment: ""), + cellID: ProgressViewKind.defaultProgress.rawValue, + configHandler: configureDefaultStyleProgressView), + CaseElement(title: NSLocalizedString("ProgressBarTitle", comment: ""), + cellID: ProgressViewKind.barProgress.rawValue, + configHandler: configureBarStyleProgressView), + ]) + + if traitCollection.userInterfaceIdiom != .mac { + // Tinted progress views available only on iOS. + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("ProgressTintedTitle", comment: ""), + cellID: ProgressViewKind.tintedProgress.rawValue, + configHandler: configureTintedProgressView), + ]) + } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + /** Reset the `completedUnitCount` of the `NSProgress` object and create + a repeating timer to increment it over time. + */ + progress.completedUnitCount = 0 + + updateTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { _ in + /** Update the `completedUnitCount` of the `NSProgress` object if it's + not completed. Otherwise, stop the timer. + */ + if self.progress.completedUnitCount < self.progress.totalUnitCount { + self.progress.completedUnitCount += 1 + } else { + self.updateTimer?.invalidate() + } + }) + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + // Stop the timer from firing. + updateTimer?.invalidate() + } + + // MARK: - Configuration + + func configureDefaultStyleProgressView(_ progressView: UIProgressView) { + progressView.progressViewStyle = .default + + // Reset the completed progress of the `UIProgressView`s. + progressView.setProgress(0.0, animated: false) + + progressViews.append(progressView) + } + + func configureBarStyleProgressView(_ progressView: UIProgressView) { + progressView.progressViewStyle = .bar + + // Reset the completed progress of the `UIProgressView`s. + progressView.setProgress(0.0, animated: false) + + progressViews.append(progressView) + } + + func configureTintedProgressView(_ progressView: UIProgressView) { + progressView.progressViewStyle = .default + + progressView.trackTintColor = UIColor.systemBlue + progressView.progressTintColor = UIColor.systemPurple + + // Reset the completed progress of the `UIProgressView`s. + progressView.setProgress(0.0, animated: false) + + progressViews.append(progressView) + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/SceneDelegate.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/SceneDelegate.swift new file mode 100755 index 000000000..a2d76dc3d --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/SceneDelegate.swift @@ -0,0 +1,84 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + This class demonstrates how to use the scene delegate to configure a scene's interface. + */ + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDelegate { + var window: UIWindow? + + /** Applications configure their UIWindow and attach the UIWindow to the provided UIWindowScene scene. + + Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + + If using a storyboard file, as specified by the Info.plist key `UISceneStoryboardFile`, + the window property automatically configures and attaches to the windowScene. + + Remember to retain the SceneDelegate's UIWindow. + The recommended approach is for the SceneDelegate to retain the scene's window. + */ + func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) { + guard (scene as? UIWindowScene) != nil else { return } + + if let splitViewController = window!.rootViewController as? UISplitViewController { + splitViewController.delegate = self + + if let navController = splitViewController.viewControllers[1] as? UINavigationController { + // For the Mac, remove the navigation bar. + if navController.traitCollection.userInterfaceIdiom == .mac { + navController.navigationBar.isHidden = true + } + } + } + } + + /** Called by iOS when the system is releasing the scene or on window close. + This occurs shortly after the scene enters the background, or when discarding its session. + Release any resources for this scene that you can create the next time the scene connects. + The scene may reconnect later because the system doesn't necessarily discard its session + (see `application:didDiscardSceneSessions` instead). + */ + func sceneDidDisconnect(_: UIScene) {} + + /** Called by iOS as the scene transitions from the background to the foreground, on window open, or on iOS resume. + Use this method to undo the changes that occur on entering the background. + */ + func sceneWillEnterForeground(_: UIScene) {} + + /** Called by iOS as the scene transitions from the foreground to the background. + Use this method to save data, release shared resources, and store enough scene-specific state information + to restore the scene to its current state. + */ + func sceneDidEnterBackground(_: UIScene) {} + + /** Called by iOS when the scene is about to move from an active state to an inactive state, on window close or on iOS enter background. + This may occur due to temporary interruptions (such as, an incoming phone call). + */ + func sceneWillResignActive(_: UIScene) {} + + /** Called by iOS when the scene after the scene moves from an inactive state to an active state. + Use this method to restart any paused tasks (or pending tasks) when the scene is inactive. + This is called every time a scene becomes active, so set up your scene UI here. + */ + func sceneDidBecomeActive(_: UIScene) {} + + func splitViewController(_: UISplitViewController, + topColumnForCollapsingToProposedTopColumn _: UISplitViewController.Column) + -> UISplitViewController.Column + { + .primary + } + + func splitViewController(_ svc: UISplitViewController, + displayModeForExpandingToProposedDisplayMode _: UISplitViewController.DisplayMode) + -> UISplitViewController.DisplayMode + { + if let navController = svc.viewControllers[0] as? UINavigationController { + navController.popToRootViewController(animated: false) + } + return .automatic + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/SegmentedControlViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/SegmentedControlViewController.swift new file mode 100755 index 000000000..a87dbe593 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/SegmentedControlViewController.swift @@ -0,0 +1,187 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A view controller that demonstrates how to use `UISegmentedControl`. + */ + +import UIKit + +class SegmentedControlViewController: BaseTableViewController { + // Cell identifier for each segmented control table view cell. + enum SegmentKind: String, CaseIterable { + case segmentDefault + case segmentTinted + case segmentCustom + case segmentCustomBackground + case segmentAction + } + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("DefaultTitle", comment: ""), + cellID: SegmentKind.segmentDefault.rawValue, + configHandler: configureDefaultSegmentedControl), + CaseElement(title: NSLocalizedString("CustomSegmentsTitle", comment: ""), + cellID: SegmentKind.segmentCustom.rawValue, + configHandler: configureCustomSegmentsSegmentedControl), + CaseElement(title: NSLocalizedString("CustomBackgroundTitle", comment: ""), + cellID: SegmentKind.segmentCustomBackground.rawValue, + configHandler: configureCustomBackgroundSegmentedControl), + CaseElement(title: NSLocalizedString("ActionBasedTitle", comment: ""), + cellID: SegmentKind.segmentAction.rawValue, + configHandler: configureActionBasedSegmentedControl), + ]) + if traitCollection.userInterfaceIdiom != .mac { + // Tinted segmented control is only available on iOS. + testCells.append(contentsOf: [ + CaseElement(title: "Tinted", + cellID: SegmentKind.segmentTinted.rawValue, + configHandler: configureTintedSegmentedControl), + ]) + } + } + + // MARK: - Configuration + + func configureDefaultSegmentedControl(_ segmentedControl: UISegmentedControl) { + // As a demonstration, disable the first segment. + segmentedControl.setEnabled(false, forSegmentAt: 0) + + segmentedControl.addTarget(self, action: #selector(SegmentedControlViewController.selectedSegmentDidChange(_:)), for: .valueChanged) + } + + func configureTintedSegmentedControl(_ segmentedControl: UISegmentedControl) { + // Use a dynamic tinted "green" color (separate one for Light Appearance and separate one for Dark Appearance). + segmentedControl.selectedSegmentTintColor = UIColor(named: "tinted_segmented_control")! + segmentedControl.selectedSegmentIndex = 1 + + segmentedControl.addTarget(self, action: #selector(SegmentedControlViewController.selectedSegmentDidChange(_:)), for: .valueChanged) + } + + func configureCustomSegmentsSegmentedControl(_ segmentedControl: UISegmentedControl) { + let airplaneImage = UIImage(systemName: "airplane") + airplaneImage?.accessibilityLabel = NSLocalizedString("Airplane", comment: "") + segmentedControl.setImage(airplaneImage, forSegmentAt: 0) + + let giftImage = UIImage(systemName: "gift") + giftImage?.accessibilityLabel = NSLocalizedString("Gift", comment: "") + segmentedControl.setImage(giftImage, forSegmentAt: 1) + + let burstImage = UIImage(systemName: "burst") + burstImage?.accessibilityLabel = NSLocalizedString("Burst", comment: "") + segmentedControl.setImage(burstImage, forSegmentAt: 2) + + segmentedControl.selectedSegmentIndex = 0 + + segmentedControl.addTarget(self, action: #selector(SegmentedControlViewController.selectedSegmentDidChange(_:)), for: .valueChanged) + } + + // Utility function to resize an image to a particular size. + func scaledImage(_ image: UIImage, scaledToSize newSize: CGSize) -> UIImage { + UIGraphicsBeginImageContextWithOptions(newSize, false, 0.0) + image.draw(in: CGRect(x: 0, y: 0, width: newSize.width, height: newSize.height)) + let newImage = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext() + return newImage + } + + // Configure the segmented control with a background image, dividers, and custom font. + // The background image first needs to be sized to match the control's size. + // + func configureCustomBackgroundSegmentedControl(_ placeHolderView: UIView) { + let customBackgroundSegmentedControl = + UISegmentedControl(items: [NSLocalizedString("CheckTitle", comment: ""), + NSLocalizedString("SearchTitle", comment: ""), + NSLocalizedString("ToolsTitle", comment: "")]) + customBackgroundSegmentedControl.selectedSegmentIndex = 2 + + // Place this custom segmented control within the placeholder view. + customBackgroundSegmentedControl.frame.size.width = placeHolderView.frame.size.width + customBackgroundSegmentedControl.frame.origin.y = + (placeHolderView.bounds.size.height - customBackgroundSegmentedControl.bounds.size.height) / 2 + placeHolderView.addSubview(customBackgroundSegmentedControl) + + // Set the background images for each control state. + let normalSegmentBackgroundImage = UIImage(named: "background") + // Size the background image to match the bounds of the segmented control. + let backgroundImageSize = customBackgroundSegmentedControl.bounds.size + let newBackgroundImageSize = scaledImage(normalSegmentBackgroundImage!, scaledToSize: backgroundImageSize) + customBackgroundSegmentedControl.setBackgroundImage(newBackgroundImageSize, for: .normal, barMetrics: .default) + + let disabledSegmentBackgroundImage = UIImage(named: "background_disabled") + customBackgroundSegmentedControl.setBackgroundImage(disabledSegmentBackgroundImage, for: .disabled, barMetrics: .default) + + let highlightedSegmentBackgroundImage = UIImage(named: "background_highlighted") + customBackgroundSegmentedControl.setBackgroundImage(highlightedSegmentBackgroundImage, for: .highlighted, barMetrics: .default) + + // Set the divider image. + let segmentDividerImage = UIImage(named: "stepper_and_segment_divider") + customBackgroundSegmentedControl.setDividerImage(segmentDividerImage, + forLeftSegmentState: .normal, + rightSegmentState: .normal, + barMetrics: .default) + + // Create a font to use for the attributed title, for both normal and highlighted states. + let font = UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body), size: 0) + let normalTextAttributes = [ + NSAttributedString.Key.foregroundColor: UIColor.systemPurple, + NSAttributedString.Key.font: font, + ] + customBackgroundSegmentedControl.setTitleTextAttributes(normalTextAttributes, for: .normal) + + let highlightedTextAttributes = [ + NSAttributedString.Key.foregroundColor: UIColor.systemGreen, + NSAttributedString.Key.font: font, + ] + customBackgroundSegmentedControl.setTitleTextAttributes(highlightedTextAttributes, for: .highlighted) + + customBackgroundSegmentedControl.addTarget(self, + action: #selector(SegmentedControlViewController.selectedSegmentDidChange(_:)), + for: .valueChanged) + } + + func configureActionBasedSegmentedControl(_ segmentedControl: UISegmentedControl) { + segmentedControl.selectedSegmentIndex = 0 + let firstAction = + UIAction(title: NSLocalizedString("CheckTitle", comment: "")) { action in + Swift.debugPrint("Segment Action '\(action.title)'") + } + segmentedControl.setAction(firstAction, forSegmentAt: 0) + let secondAction = + UIAction(title: NSLocalizedString("SearchTitle", comment: "")) { action in + Swift.debugPrint("Segment Action '\(action.title)'") + } + segmentedControl.setAction(secondAction, forSegmentAt: 1) + let thirdAction = + UIAction(title: NSLocalizedString("ToolsTitle", comment: "")) { action in + Swift.debugPrint("Segment Action '\(action.title)'") + } + segmentedControl.setAction(thirdAction, forSegmentAt: 2) + } + + // MARK: - Actions + + @objc + func selectedSegmentDidChange(_ segmentedControl: UISegmentedControl) { + Swift.debugPrint("The selected segment: \(segmentedControl.selectedSegmentIndex).") + } + + // MARK: - UITableViewDataSource + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cellTest = testCells[indexPath.section] + let cell = tableView.dequeueReusableCell(withIdentifier: cellTest.cellID, for: indexPath) + if let segementedControl = cellTest.targetView(cell) as? UISegmentedControl { + cellTest.configHandler(segementedControl) + } else if let placeHolderView = cellTest.targetView(cell) { + // The only non-segmented control cell has a placeholder UIView (for adding one as a subview). + cellTest.configHandler(placeHolderView) + } + return cell + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/SliderViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/SliderViewController.swift new file mode 100755 index 000000000..b1d73dbdd --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/SliderViewController.swift @@ -0,0 +1,144 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A view controller that demonstrates how to use `UISlider`. + */ + +import UIKit + +class SliderViewController: BaseTableViewController { + // Cell identifier for each slider table view cell. + enum SliderKind: String, CaseIterable { + case sliderDefault + case sliderTinted + case sliderCustom + case sliderMaxMinImage + } + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("DefaultTitle", comment: ""), + cellID: SliderKind.sliderDefault.rawValue, + configHandler: configureDefaultSlider), + ]) + + if #available(iOS 15, *) { + // These cases require iOS 15 or later when running on Mac Catalyst. + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("CustomTitle", comment: ""), + cellID: SliderKind.sliderCustom.rawValue, + configHandler: configureCustomSlider), + ]) + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("MinMaxImagesTitle", comment: ""), + cellID: SliderKind.sliderMaxMinImage.rawValue, + configHandler: configureMinMaxImageSlider), + ]) + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("TintedTitle", comment: ""), + cellID: SliderKind.sliderTinted.rawValue, + configHandler: configureTintedSlider), + ]) + } + } + + // MARK: - Configuration + + func configureDefaultSlider(_ slider: UISlider) { + slider.minimumValue = 0 + slider.maximumValue = 100 + slider.value = 42 + slider.isContinuous = true + + slider.addTarget(self, action: #selector(SliderViewController.sliderValueDidChange(_:)), for: .valueChanged) + } + + @available(iOS 15.0, *) + func configureTintedSlider(slider: UISlider) { + /** To keep the look the same betwen iOS and macOS: + For minimumTrackTintColor, maximumTrackTintColor to work in Mac Catalyst, use UIBehavioralStyle as ".pad", + Available in macOS 12 or later (Mac Catalyst 15.0 or later). + Use this for controls that need to look the same between iOS and macOS. + */ + if traitCollection.userInterfaceIdiom == .mac { + slider.preferredBehavioralStyle = .pad + } + + slider.minimumTrackTintColor = UIColor.systemBlue + slider.maximumTrackTintColor = UIColor.systemPurple + + slider.addTarget(self, action: #selector(SliderViewController.sliderValueDidChange(_:)), for: .valueChanged) + } + + @available(iOS 15.0, *) + func configureCustomSlider(slider: UISlider) { + /** To keep the look the same betwen iOS and macOS: + For setMinimumTrackImage, setMaximumTrackImage, setThumbImage to work in Mac Catalyst, use UIBehavioralStyle as ".pad", + Available in macOS 12 or later (Mac Catalyst 15.0 or later). + Use this for controls that need to look the same between iOS and macOS. + */ + if traitCollection.userInterfaceIdiom == .mac { + slider.preferredBehavioralStyle = .pad + } + + let leftTrackImage = UIImage(named: "slider_blue_track") + slider.setMinimumTrackImage(leftTrackImage, for: .normal) + + let rightTrackImage = UIImage(named: "slider_green_track") + slider.setMaximumTrackImage(rightTrackImage, for: .normal) + + // Set the sliding thumb image (normal and highlighted). + // + // For fun, choose a different image symbol configuraton for the thumb's image between macOS and iOS. + var thumbImageConfig: UIImage.SymbolConfiguration + if slider.traitCollection.userInterfaceIdiom == .mac { + thumbImageConfig = UIImage.SymbolConfiguration(scale: .large) + } else { + thumbImageConfig = UIImage.SymbolConfiguration(pointSize: 30, weight: .heavy, scale: .large) + } + let thumbImage = UIImage(systemName: "circle.fill", withConfiguration: thumbImageConfig) + slider.setThumbImage(thumbImage, for: .normal) + + let thumbImageHighlighted = UIImage(systemName: "circle", withConfiguration: thumbImageConfig) + slider.setThumbImage(thumbImageHighlighted, for: .highlighted) + + // Set the rest of the slider's attributes. + slider.minimumValue = 0 + slider.maximumValue = 100 + slider.isContinuous = false + slider.value = 84 + + slider.addTarget(self, action: #selector(SliderViewController.sliderValueDidChange(_:)), for: .valueChanged) + } + + func configureMinMaxImageSlider(slider: UISlider) { + /** To keep the look the same betwen iOS and macOS: + For setMinimumValueImage, setMaximumValueImage to work in Mac Catalyst, use UIBehavioralStyle as ".pad", + Available in macOS 12 or later (Mac Catalyst 15.0 or later). + Use this for controls that need to look the same between iOS and macOS. + */ + if #available(iOS 15, *) { + if traitCollection.userInterfaceIdiom == .mac { + slider.preferredBehavioralStyle = .pad + } + } + + slider.minimumValueImage = UIImage(systemName: "tortoise") + slider.maximumValueImage = UIImage(systemName: "hare") + + slider.addTarget(self, action: #selector(SliderViewController.sliderValueDidChange(_:)), for: .valueChanged) + } + + // MARK: - Actions + + @objc + func sliderValueDidChange(_ slider: UISlider) { + let formattedValue = String(format: "%.2f", slider.value) + Swift.debugPrint("Slider changed its value: \(formattedValue)") + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/StackViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/StackViewController.swift new file mode 100755 index 000000000..30aca2c22 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/StackViewController.swift @@ -0,0 +1,98 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A view controller that demonstrates different options for manipulating `UIStackView` content. + */ + +import UIKit + +class StackViewController: UIViewController { + // MARK: - Properties + + @IBOutlet var furtherDetailStackView: UIStackView! + @IBOutlet var plusButton: UIButton! + @IBOutlet var addRemoveExampleStackView: UIStackView! + @IBOutlet var addArrangedViewButton: UIButton! + @IBOutlet var removeArrangedViewButton: UIButton! + + let maximumArrangedSubviewCount = 3 + + // MARK: - View Life Cycle + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + furtherDetailStackView.isHidden = true + plusButton.isHidden = false + updateAddRemoveButtons() + } + + // MARK: - Actions + + @IBAction func showFurtherDetail(_: AnyObject) { + // Animate the changes by performing them in a `UIViewPropertyAnimator` animation block. + let showDetailAnimator = UIViewPropertyAnimator(duration: 0.25, curve: .easeIn, animations: { [weak self] in + // Reveal the further details stack view and hide the plus button. + self?.furtherDetailStackView.isHidden = false + self?.plusButton.isHidden = true + }) + showDetailAnimator.startAnimation() + } + + @IBAction func hideFurtherDetail(_: AnyObject) { + // Animate the changes by performing them in a `UIViewPropertyAnimator` animation block. + let hideDetailAnimator = UIViewPropertyAnimator(duration: 0.25, curve: .easeOut, animations: { [weak self] in + // Reveal the further details stack view and hide the plus button. + self?.furtherDetailStackView.isHidden = true + self?.plusButton.isHidden = false + }) + hideDetailAnimator.startAnimation() + } + + @IBAction func addArrangedSubviewToStack(_: AnyObject) { + // Create a simple, fixed-size, square view to add to the stack view. + let newViewSize = CGSize(width: 38, height: 38) + let newView = UIView(frame: CGRect(origin: CGPoint.zero, size: newViewSize)) + newView.backgroundColor = randomColor() + newView.widthAnchor.constraint(equalToConstant: newViewSize.width).isActive = true + newView.heightAnchor.constraint(equalToConstant: newViewSize.height).isActive = true + + // Adding an arranged subview automatically adds it as a child of the stack view. + addRemoveExampleStackView.addArrangedSubview(newView) + + updateAddRemoveButtons() + } + + @IBAction func removeArrangedSubviewFromStack(_: AnyObject) { + // Make sure there is an arranged view to remove. + guard let viewToRemove = addRemoveExampleStackView.arrangedSubviews.last else { return } + + addRemoveExampleStackView.removeArrangedSubview(viewToRemove) + + /** Calling `removeArrangedSubview` does not remove the provided view from + the stack view's `subviews` array. Since we no longer want the view + we removed to appear, we have to explicitly remove it from its superview. + */ + viewToRemove.removeFromSuperview() + + updateAddRemoveButtons() + } + + // MARK: - Convenience + + func updateAddRemoveButtons() { + let arrangedSubviewCount = addRemoveExampleStackView.arrangedSubviews.count + + addArrangedViewButton.isEnabled = arrangedSubviewCount < maximumArrangedSubviewCount + removeArrangedViewButton.isEnabled = arrangedSubviewCount > 0 + } + + func randomColor() -> UIColor { + let red = CGFloat(arc4random_uniform(255)) / 255.0 + let green = CGFloat(arc4random_uniform(255)) / 255.0 + let blue = CGFloat(arc4random_uniform(255)) / 255.0 + + return UIColor(red: red, green: green, blue: blue, alpha: 1.0) + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/StepperViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/StepperViewController.swift new file mode 100755 index 000000000..a248bf66a --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/StepperViewController.swift @@ -0,0 +1,96 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A view controller that demonstrates how to use `UIStepper`. + */ + +import UIKit + +class StepperViewController: BaseTableViewController { + // Cell identifier for each stepper table view cell. + enum StepperKind: String, CaseIterable { + case defaultStepper + case tintedStepper + case customStepper + } + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("DefaultStepperTitle", comment: ""), + cellID: StepperKind.defaultStepper.rawValue, + configHandler: configureDefaultStepper), + CaseElement(title: NSLocalizedString("TintedStepperTitle", comment: ""), + cellID: StepperKind.tintedStepper.rawValue, + configHandler: configureTintedStepper), + CaseElement(title: NSLocalizedString("CustomStepperTitle", comment: ""), + cellID: StepperKind.customStepper.rawValue, + configHandler: configureCustomStepper), + ]) + } + + // MARK: - Configuration + + func configureDefaultStepper(stepper: UIStepper) { + // Setup the stepper range 0 to 10, initial value 0, increment/decrement factor of 1. + stepper.value = 0 + stepper.minimumValue = 0 + stepper.maximumValue = 10 + stepper.stepValue = 1 + + stepper.addTarget(self, + action: #selector(StepperViewController.stepperValueDidChange(_:)), + for: .valueChanged) + } + + func configureTintedStepper(stepper: UIStepper) { + // Setup the stepper range 0 to 20, initial value 20, increment/decrement factor of 1. + stepper.value = 20 + stepper.minimumValue = 0 + stepper.maximumValue = 20 + stepper.stepValue = 1 + + stepper.tintColor = UIColor(named: "tinted_stepper_control")! + stepper.setDecrementImage(stepper.decrementImage(for: .normal), for: .normal) + stepper.setIncrementImage(stepper.incrementImage(for: .normal), for: .normal) + + stepper.addTarget(self, + action: #selector(StepperViewController.stepperValueDidChange(_:)), + for: .valueChanged) + } + + func configureCustomStepper(stepper: UIStepper) { + // Set the background image. + let stepperBackgroundImage = UIImage(named: "background") + stepper.setBackgroundImage(stepperBackgroundImage, for: .normal) + + let stepperHighlightedBackgroundImage = UIImage(named: "background_highlighted") + stepper.setBackgroundImage(stepperHighlightedBackgroundImage, for: .highlighted) + + let stepperDisabledBackgroundImage = UIImage(named: "background_disabled") + stepper.setBackgroundImage(stepperDisabledBackgroundImage, for: .disabled) + + // Set the image which will be painted in between the two stepper segments. It depends on the states of both segments. + let stepperSegmentDividerImage = UIImage(named: "stepper_and_segment_divider") + stepper.setDividerImage(stepperSegmentDividerImage, forLeftSegmentState: .normal, rightSegmentState: .normal) + + // Set the image for the + button. + let stepperIncrementImage = UIImage(systemName: "plus") + stepper.setIncrementImage(stepperIncrementImage, for: .normal) + + // Set the image for the - button. + let stepperDecrementImage = UIImage(systemName: "minus") + stepper.setDecrementImage(stepperDecrementImage, for: .normal) + + stepper.addTarget(self, action: #selector(StepperViewController.stepperValueDidChange(_:)), for: .valueChanged) + } + + // MARK: - Actions + + @objc + func stepperValueDidChange(_ stepper: UIStepper) { + Swift.debugPrint("A stepper changed its value: \(stepper.value).") + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/SwitchViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/SwitchViewController.swift new file mode 100755 index 000000000..30e8c1482 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/SwitchViewController.swift @@ -0,0 +1,89 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A view controller that demonstrates how to use `UISwitch`. + */ + +import UIKit + +class SwitchViewController: BaseTableViewController { + // Cell identifier for each switch table view cell. + enum SwitchKind: String, CaseIterable { + case defaultSwitch + case checkBoxSwitch + case tintedSwitch + } + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("DefaultSwitchTitle", comment: ""), + cellID: SwitchKind.defaultSwitch.rawValue, + configHandler: configureDefaultSwitch), + ]) + + // Checkbox switch is available only when running on macOS. + if navigationController!.traitCollection.userInterfaceIdiom == .mac { + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("CheckboxSwitchTitle", comment: ""), + cellID: SwitchKind.checkBoxSwitch.rawValue, + configHandler: configureCheckboxSwitch), + ]) + } + + // Tinted switch is available only when running on iOS. + if navigationController!.traitCollection.userInterfaceIdiom != .mac { + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("TintedSwitchTitle", comment: ""), + cellID: SwitchKind.tintedSwitch.rawValue, + configHandler: configureTintedSwitch), + ]) + } + } + + // MARK: - Configuration + + func configureDefaultSwitch(_ switchControl: UISwitch) { + switchControl.setOn(true, animated: false) + switchControl.preferredStyle = .sliding + + switchControl.addTarget(self, + action: #selector(SwitchViewController.switchValueDidChange(_:)), + for: .valueChanged) + } + + func configureCheckboxSwitch(_ switchControl: UISwitch) { + switchControl.setOn(true, animated: false) + + switchControl.addTarget(self, + action: #selector(SwitchViewController.switchValueDidChange(_:)), + for: .valueChanged) + + // On the Mac, make sure this control take on the apperance of a checkbox with a title. + if traitCollection.userInterfaceIdiom == .mac { + switchControl.preferredStyle = .checkbox + + // Title on a UISwitch is only supported when running Catalyst apps in the Mac Idiom. + switchControl.title = NSLocalizedString("SwitchTitle", comment: "") + } + } + + func configureTintedSwitch(_ switchControl: UISwitch) { + switchControl.tintColor = UIColor.systemBlue + switchControl.onTintColor = UIColor.systemGreen + switchControl.thumbTintColor = UIColor.systemPurple + + switchControl.addTarget(self, + action: #selector(SwitchViewController.switchValueDidChange(_:)), + for: .valueChanged) + } + + // MARK: - Actions + + @objc + func switchValueDidChange(_ aSwitch: UISwitch) { + Swift.debugPrint("A switch changed its value: \(aSwitch.isOn).") + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/SymbolViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/SymbolViewController.swift new file mode 100755 index 000000000..6f7d08f8e --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/SymbolViewController.swift @@ -0,0 +1,104 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A view controller that demonstrates how to use SF Symbols. + */ + +import UIKit + +class SymbolViewController: BaseTableViewController { + // Cell identifier for each SF Symbol table view cell. + enum SymbolKind: String, CaseIterable { + case plainSymbol + case tintedSymbol + case largeSizeSymbol + case hierarchicalColorSymbol + case paletteColorsSymbol + case preferringMultiColorSymbol + } + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("PlainSymbolTitle", comment: ""), + cellID: SymbolKind.plainSymbol.rawValue, + configHandler: configurePlainSymbol), + CaseElement(title: NSLocalizedString("TintedSymbolTitle", comment: ""), + cellID: SymbolKind.tintedSymbol.rawValue, + configHandler: configureTintedSymbol), + CaseElement(title: NSLocalizedString("LargeSymbolTitle", comment: ""), + cellID: SymbolKind.largeSizeSymbol.rawValue, + configHandler: configureLargeSizeSymbol), + ]) + + if #available(iOS 15, *) { + // These type SF Sybols, and variants are available on iOS 15, Mac Catalyst 15 or later. + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("HierarchicalSymbolTitle", comment: ""), + cellID: SymbolKind.hierarchicalColorSymbol.rawValue, + configHandler: configureHierarchicalSymbol), + CaseElement(title: NSLocalizedString("PaletteSymbolTitle", comment: ""), + cellID: SymbolKind.paletteColorsSymbol.rawValue, + configHandler: configurePaletteColorsSymbol), + CaseElement(title: NSLocalizedString("PreferringMultiColorSymbolTitle", comment: ""), + cellID: SymbolKind.preferringMultiColorSymbol.rawValue, + configHandler: configurePreferringMultiColorSymbol), + ]) + } + } + + // MARK: - UITableViewDataSource + + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + let cellTest = testCells[indexPath.section] + let cell = tableView.dequeueReusableCell(withIdentifier: cellTest.cellID) + return cell!.contentView.bounds.size.height + } + + // MARK: - Configuration + + func configurePlainSymbol(_ imageView: UIImageView) { + let image = UIImage(systemName: "cloud.sun.rain.fill") + imageView.image = image + } + + func configureTintedSymbol(_ imageView: UIImageView) { + let image = UIImage(systemName: "cloud.sun.rain.fill") + imageView.image = image + imageView.tintColor = .systemPurple + } + + func configureLargeSizeSymbol(_ imageView: UIImageView) { + let image = UIImage(systemName: "cloud.sun.rain.fill") + imageView.image = image + let symbolConfig = UIImage.SymbolConfiguration(pointSize: 32, weight: .heavy, scale: .large) + imageView.preferredSymbolConfiguration = symbolConfig + } + + @available(iOS 15.0, *) + func configureHierarchicalSymbol(_ imageView: UIImageView) { + let imageConfig = UIImage.SymbolConfiguration(hierarchicalColor: UIColor.systemRed) + let hierarchicalSymbol = UIImage(systemName: "cloud.sun.rain.fill") + imageView.image = hierarchicalSymbol + imageView.preferredSymbolConfiguration = imageConfig + } + + @available(iOS 15.0, *) + func configurePaletteColorsSymbol(_ imageView: UIImageView) { + let palleteSymbolConfig = UIImage.SymbolConfiguration(paletteColors: [UIColor.systemRed, UIColor.systemOrange, UIColor.systemYellow]) + let palleteSymbol = UIImage(systemName: "battery.100.bolt") + imageView.image = palleteSymbol + imageView.backgroundColor = UIColor.darkText + imageView.preferredSymbolConfiguration = palleteSymbolConfig + } + + @available(iOS 15.0, *) + func configurePreferringMultiColorSymbol(_ imageView: UIImageView) { + let preferredSymbolConfig = UIImage.SymbolConfiguration.preferringMulticolor() + let preferredSymbol = UIImage(systemName: "circle.hexagongrid.fill") + imageView.image = preferredSymbol + imageView.preferredSymbolConfiguration = preferredSymbolConfig + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/TextFieldViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/TextFieldViewController.swift new file mode 100755 index 000000000..c3f201046 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/TextFieldViewController.swift @@ -0,0 +1,160 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A view controller that demonstrates how to use `UITextField`. + */ + +import UIKit + +class TextFieldViewController: BaseTableViewController { + // Cell identifier for each text field table view cell. + enum TextFieldKind: String, CaseIterable { + case textField + case tintedTextField + case secureTextField + case specificKeyboardTextField + case customTextField + case searchTextField + } + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("DefaultTextFieldTitle", comment: ""), + cellID: TextFieldKind.textField.rawValue, + configHandler: configureTextField), + CaseElement(title: NSLocalizedString("TintedTextFieldTitle", comment: ""), + cellID: TextFieldKind.tintedTextField.rawValue, + configHandler: configureTintedTextField), + CaseElement(title: NSLocalizedString("SecuretTextFieldTitle", comment: ""), + cellID: TextFieldKind.secureTextField.rawValue, + configHandler: configureSecureTextField), + CaseElement(title: NSLocalizedString("SearchTextFieldTitle", comment: ""), + cellID: TextFieldKind.searchTextField.rawValue, + configHandler: configureSearchTextField), + ]) + + if traitCollection.userInterfaceIdiom != .mac { + testCells.append(contentsOf: [ + // Show text field with specific kind of keyboard for iOS only. + CaseElement(title: NSLocalizedString("SpecificKeyboardTextFieldTitle", comment: ""), + cellID: TextFieldKind.specificKeyboardTextField.rawValue, + configHandler: configureSpecificKeyboardTextField), + + // Show text field with custom background for iOS only. + CaseElement(title: NSLocalizedString("CustomTextFieldTitle", comment: ""), + cellID: TextFieldKind.customTextField.rawValue, + configHandler: configureCustomTextField), + ]) + } + } + + // MARK: - Configuration + + func configureTextField(_ textField: UITextField) { + textField.placeholder = NSLocalizedString("Placeholder text", comment: "") + textField.autocorrectionType = .yes + textField.returnKeyType = .done + textField.clearButtonMode = .whileEditing + } + + func configureTintedTextField(_ textField: UITextField) { + textField.tintColor = UIColor.systemBlue + textField.textColor = UIColor.systemGreen + + textField.placeholder = NSLocalizedString("Placeholder text", comment: "") + textField.returnKeyType = .done + textField.clearButtonMode = .never + } + + func configureSecureTextField(_ textField: UITextField) { + textField.isSecureTextEntry = true + + textField.placeholder = NSLocalizedString("Placeholder text", comment: "") + textField.returnKeyType = .done + textField.clearButtonMode = .always + } + + func configureSearchTextField(_ textField: UITextField) { + if let searchField = textField as? UISearchTextField { + searchField.placeholder = NSLocalizedString("Enter search text", comment: "") + searchField.returnKeyType = .done + searchField.clearButtonMode = .always + searchField.allowsDeletingTokens = true + + // Setup the left view as a symbol image view. + let searchIcon = UIImageView(image: UIImage(systemName: "magnifyingglass")) + searchIcon.tintColor = UIColor.systemGray + searchField.leftView = searchIcon + searchField.leftViewMode = .always + + let secondToken = UISearchToken(icon: UIImage(systemName: "staroflife"), text: "Token 2") + searchField.insertToken(secondToken, at: 0) + + let firstToken = UISearchToken(icon: UIImage(systemName: "staroflife.fill"), text: "Token 1") + searchField.insertToken(firstToken, at: 0) + } + } + + /** There are many different types of keyboards that you may choose to use. + The different types of keyboards are defined in the `UITextInputTraits` interface. + This example shows how to display a keyboard to help enter email addresses. + */ + func configureSpecificKeyboardTextField(_ textField: UITextField) { + textField.keyboardType = .emailAddress + + textField.placeholder = NSLocalizedString("Placeholder text", comment: "") + textField.returnKeyType = .done + } + + func configureCustomTextField(_ textField: UITextField) { + // Text fields with custom image backgrounds must have no border. + textField.borderStyle = .none + + textField.background = UIImage(named: "text_field_background") + + // Create a purple button to be used as the right view of the custom text field. + let purpleImage = UIImage(named: "text_field_purple_right_view")! + let purpleImageButton = UIButton(type: .custom) + purpleImageButton.bounds = CGRect(x: 0, y: 0, width: purpleImage.size.width, height: purpleImage.size.height) + purpleImageButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 5) + purpleImageButton.setImage(purpleImage, for: .normal) + purpleImageButton.addTarget(self, action: #selector(TextFieldViewController.customTextFieldPurpleButtonClicked), for: .touchUpInside) + textField.rightView = purpleImageButton + textField.rightViewMode = .always + + textField.placeholder = NSLocalizedString("Placeholder text", comment: "") + textField.autocorrectionType = .no + textField.clearButtonMode = .never + textField.returnKeyType = .done + } + + // MARK: - Actions + + @objc + func customTextFieldPurpleButtonClicked() { + debugPrint("The custom text field's purple right view button was clicked.") + } +} + +// Custom text field for controlling input text placement. +class CustomTextField: UITextField { + let leftMarginPadding: CGFloat = 12 + let rightMarginPadding: CGFloat = 36 + + override func textRect(forBounds bounds: CGRect) -> CGRect { + var rect = bounds + rect.origin.x += leftMarginPadding + rect.size.width -= rightMarginPadding + return rect + } + + override func editingRect(forBounds bounds: CGRect) -> CGRect { + var rect = bounds + rect.origin.x += leftMarginPadding + rect.size.width -= rightMarginPadding + return rect + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/TextViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/TextViewController.swift new file mode 100755 index 000000000..a579777ed --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/TextViewController.swift @@ -0,0 +1,194 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A view controller that demonstrates how to use `UITextView`. + */ + +import UIKit + +class TextViewController: UIViewController { + // MARK: - Properties + + @IBOutlet var textView: UITextView! + + /// Used to adjust the text view's height when the keyboard hides and shows. + @IBOutlet var textViewBottomLayoutGuideConstraint: NSLayoutConstraint! + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + configureTextView() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + // Listen for changes to keyboard visibility so that we can adjust the text view's height accordingly. + let notificationCenter = NotificationCenter.default + + notificationCenter.addObserver(self, + selector: #selector(TextViewController.handleKeyboardNotification(_:)), + name: UIResponder.keyboardWillShowNotification, + object: nil) + + notificationCenter.addObserver(self, + selector: #selector(TextViewController.handleKeyboardNotification(_:)), + name: UIResponder.keyboardWillHideNotification, + object: nil) + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + let notificationCenter = NotificationCenter.default + notificationCenter.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) + notificationCenter.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) + } + + // MARK: - Keyboard Event Notifications + + @objc + func handleKeyboardNotification(_ notification: Notification) { + guard let userInfo = notification.userInfo else { return } + + // Get the animation duration. + var animationDuration: TimeInterval = 0 + if let value = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber { + animationDuration = value.doubleValue + } + + // Convert the keyboard frame from screen to view coordinates. + var keyboardScreenBeginFrame = CGRect() + if let value = (userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue) { + keyboardScreenBeginFrame = value.cgRectValue + } + + var keyboardScreenEndFrame = CGRect() + if let value = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue) { + keyboardScreenEndFrame = value.cgRectValue + } + + let keyboardViewBeginFrame = view.convert(keyboardScreenBeginFrame, from: view.window) + let keyboardViewEndFrame = view.convert(keyboardScreenEndFrame, from: view.window) + + let originDelta = keyboardViewEndFrame.origin.y - keyboardViewBeginFrame.origin.y + + // The text view should be adjusted, update the constant for this constraint. + textViewBottomLayoutGuideConstraint.constant -= originDelta + + // Inform the view that its autolayout constraints have changed and the layout should be updated. + view.setNeedsUpdateConstraints() + + // Animate updating the view's layout by calling layoutIfNeeded inside a `UIViewPropertyAnimator` animation block. + let textViewAnimator = UIViewPropertyAnimator(duration: animationDuration, curve: .easeIn, animations: { [weak self] in + self?.view.layoutIfNeeded() + }) + textViewAnimator.startAnimation() + + // Scroll to the selected text once the keyboard frame changes. + let selectedRange = textView.selectedRange + textView.scrollRangeToVisible(selectedRange) + } + + // MARK: - Configuration + + func reflowTextAttributes() { + var entireTextColor = UIColor.black + + // The text should be white in dark mode. + if view.traitCollection.userInterfaceStyle == .dark { + entireTextColor = UIColor.white + } + let entireAttributedText = NSMutableAttributedString(attributedString: textView.attributedText!) + let entireRange = NSRange(location: 0, length: entireAttributedText.length) + entireAttributedText.addAttribute(NSAttributedString.Key.foregroundColor, value: entireTextColor, range: entireRange) + textView.attributedText = entireAttributedText + + /** Modify some of the attributes of the attributed string. + You can modify these attributes yourself to get a better feel for what they do. + Note that the initial text is visible in the storyboard. + */ + let attributedText = NSMutableAttributedString(attributedString: textView.attributedText!) + + /** Use NSString so the result of rangeOfString is an NSRange, not Range. + This will then be the correct type to then pass to the addAttribute method of NSMutableAttributedString. + */ + let text = textView.text! as NSString + + // Find the range of each element to modify. + let highlightedRange = text.range(of: NSLocalizedString("highlighted", comment: "")) + let underlinedRange = text.range(of: NSLocalizedString("underlined", comment: "")) + let tintedRange = text.range(of: NSLocalizedString("tinted", comment: "")) + + // Add highlight attribute. + attributedText.addAttribute(NSAttributedString.Key.backgroundColor, value: UIColor.systemGreen, range: highlightedRange) + + // Add underline attribute. + attributedText.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: underlinedRange) + + // Add tint color. + attributedText.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.systemBlue, range: tintedRange) + + textView.attributedText = attributedText + } + + override func traitCollectionDidChange(_: UITraitCollection?) { + // With the background change, we need to re-apply the text attributes. + reflowTextAttributes() + } + + func symbolAttributedString(name: String) -> NSAttributedString { + let symbolAttachment = NSTextAttachment() + if let symbolImage = UIImage(systemName: name)?.withRenderingMode(.alwaysTemplate) { + symbolAttachment.image = symbolImage + } + return NSAttributedString(attachment: symbolAttachment) + } + + @available(iOS 15.0, *) + func multiColorSymbolAttributedString(name: String) -> NSAttributedString { + let symbolAttachment = NSTextAttachment() + let palleteSymbolConfig = UIImage.SymbolConfiguration(paletteColors: [UIColor.systemOrange, UIColor.systemRed]) + if let symbolImage = UIImage(systemName: name)?.withConfiguration(palleteSymbolConfig) { + symbolAttachment.image = symbolImage + } + return NSAttributedString(attachment: symbolAttachment) + } + + func configureTextView() { + let bodyFont = UIFont.systemFont(ofSize: 16) + + textView.font = bodyFont + textView.backgroundColor = UIColor(named: "text_view_background") + textView.isScrollEnabled = true + textView.isEditable = true + + textView.text = "Some text with symbols: ❤️❤️❤️" + } + + // MARK: - Actions + + @objc + func doneBarButtonItemClicked() { + // Dismiss the keyboard by removing it as the first responder. + textView.resignFirstResponder() + + navigationItem.setRightBarButton(nil, animated: true) + } +} + +// MARK: - UITextViewDelegate + +extension TextViewController: UITextViewDelegate { + func textViewDidBeginEditing(_: UITextView) { + // Provide a "Done" button for the user to end text editing. + let doneBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, + target: self, + action: #selector(TextViewController.doneBarButtonItemClicked)) + + navigationItem.setRightBarButton(doneBarButtonItem, animated: true) + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/TintedToolbarViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/TintedToolbarViewController.swift new file mode 100755 index 000000000..5ebf71e06 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/TintedToolbarViewController.swift @@ -0,0 +1,75 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A view controller that demonstrates how to customize a `UIToolbar`. + */ + +import UIKit + +class TintedToolbarViewController: UIViewController { + // MARK: - Properties + + @IBOutlet var toolbar: UIToolbar! + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + // See the `UIBarStyle` enum for more styles, including `.Default`. + toolbar.barStyle = .black + toolbar.isTranslucent = false + + toolbar.tintColor = UIColor.systemGreen + toolbar.backgroundColor = UIColor.systemBlue + + let toolbarButtonItems = [ + refreshBarButtonItem, + flexibleSpaceBarButtonItem, + actionBarButtonItem, + ] + toolbar.setItems(toolbarButtonItems, animated: true) + } + + // MARK: - `UIBarButtonItem` Creation and Configuration + + var refreshBarButtonItem: UIBarButtonItem { + UIBarButtonItem(barButtonSystemItem: .refresh, + target: self, + action: #selector(TintedToolbarViewController.barButtonItemClicked(_:))) + } + + var flexibleSpaceBarButtonItem: UIBarButtonItem { + // Note that there's no target/action since this represents empty space. + UIBarButtonItem(barButtonSystemItem: .flexibleSpace, + target: nil, + action: nil) + } + + var actionBarButtonItem: UIBarButtonItem { + UIBarButtonItem(barButtonSystemItem: .action, + target: self, + action: #selector(TintedToolbarViewController.actionBarButtonItemClicked(_:))) + } + + // MARK: - Actions + + @objc + func barButtonItemClicked(_ barButtonItem: UIBarButtonItem) { + Swift.debugPrint("A bar button item on the tinted toolbar was clicked: \(barButtonItem).") + } + + @objc + func actionBarButtonItemClicked(_ barButtonItem: UIBarButtonItem) { + if let image = UIImage(named: "Flowers_1") { + let activityItems = ["Shared piece of text", image] as [Any] + + let activityViewController = + UIActivityViewController(activityItems: activityItems, applicationActivities: nil) + + activityViewController.popoverPresentationController?.barButtonItem = barButtonItem + present(activityViewController, animated: true, completion: nil) + } + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/UIKitCatalog/Base.lproj/Localizable.stringsdict b/PostHogExampleAutocapture/PostHogExampleAutocapture/UIKitCatalog/Base.lproj/Localizable.stringsdict new file mode 100644 index 000000000..8dbf8f201 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/UIKitCatalog/Base.lproj/Localizable.stringsdict @@ -0,0 +1,41 @@ + + + + + Cart Items String + + NSStringLocalizedFormatKey + %#@items@ + items + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d item + other + %d items + + + + Cart Tooltip String + + NSStringLocalizedFormatKey + %#@items@ + items + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + zero + Cart is Empty + one + Cart has %d item + other + Cart has %d items + + + + diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/VisualEffectViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/VisualEffectViewController.swift new file mode 100755 index 000000000..57da6b41b --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/VisualEffectViewController.swift @@ -0,0 +1,68 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A view controller that demonstrates how to use `UIVisualEffectView`. + */ + +import UIKit + +class VisualEffectViewController: UIViewController { + // MARK: - Properties + + @IBOutlet var imageView: UIImageView! + + private var visualEffect: UIVisualEffectView = { + let vev = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) + vev.translatesAutoresizingMaskIntoConstraints = false + return vev + }() + + private var textView: UITextView = { + let textView = UITextView(frame: CGRect()) + textView.font = UIFont.systemFont(ofSize: 14) + textView.text = NSLocalizedString("VisualEffectTextContent", comment: "") + + textView.translatesAutoresizingMaskIntoConstraints = false + textView.backgroundColor = UIColor.clear + if let fontDescriptor = UIFontDescriptor + .preferredFontDescriptor(withTextStyle: UIFont.TextStyle.body) + .withSymbolicTraits(UIFontDescriptor.SymbolicTraits.traitLooseLeading) + { + let looseLeadingFont = UIFont(descriptor: fontDescriptor, size: 0) + textView.font = looseLeadingFont + } + return textView + }() + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + // Add the visual effect view in the same area covering the image view. + view.addSubview(visualEffect) + NSLayoutConstraint.activate([ + visualEffect.topAnchor.constraint(equalTo: imageView.topAnchor), + visualEffect.leadingAnchor.constraint(equalTo: imageView.leadingAnchor), + visualEffect.trailingAnchor.constraint(equalTo: imageView.trailingAnchor), + visualEffect.bottomAnchor.constraint(equalTo: imageView.bottomAnchor), + ]) + + // Add a text view as a subview to the visual effect view. + visualEffect.contentView.addSubview(textView) + NSLayoutConstraint.activate([ + textView.topAnchor.constraint(equalTo: visualEffect.safeAreaLayoutGuide.topAnchor), + textView.leadingAnchor.constraint(equalTo: visualEffect.safeAreaLayoutGuide.leadingAnchor), + textView.trailingAnchor.constraint(equalTo: visualEffect.safeAreaLayoutGuide.trailingAnchor), + textView.bottomAnchor.constraint(equalTo: visualEffect.safeAreaLayoutGuide.bottomAnchor), + ]) + + if #available(iOS 15, *) { + // Use UIToolTipInteraction which is available on iOS 15 or later, add it to the image view. + let toolTipString = NSLocalizedString("VisualEffectToolTipTitle", comment: "") + let interaction = UIToolTipInteraction(defaultToolTip: toolTipString) + imageView.addInteraction(interaction) + } + } +} diff --git a/PostHogExampleAutocapture/PostHogExampleAutocapture/WebViewController.swift b/PostHogExampleAutocapture/PostHogExampleAutocapture/WebViewController.swift new file mode 100755 index 000000000..f898d7270 --- /dev/null +++ b/PostHogExampleAutocapture/PostHogExampleAutocapture/WebViewController.swift @@ -0,0 +1,57 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + A view controller that demonstrates how to use `WKWebView`. + */ + +import UIKit +import WebKit + +/** NOTE: + If your app customizes, interacts with, or controls the display of web content, use the WKWebView class. + If you want to view a website from anywhere on the Internet, use the SFSafariViewController class. + */ + +class WebViewController: UIViewController { + // MARK: - Properties + + @IBOutlet var webView: WKWebView! + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + // So we can capture failures in "didFailProvisionalNavigation". + webView.navigationDelegate = self + loadAddressURL() + } + + // MARK: - Loading + + func loadAddressURL() { + // Set the content to local html in our app bundle. + if let url = Bundle.main.url(forResource: "content", withExtension: "html") { + webView.loadFileURL(url, allowingReadAccessTo: url) + } + } +} + +// MARK: - WKNavigationDelegate + +extension WebViewController: WKNavigationDelegate { + func webView(_ webView: WKWebView, didFailProvisionalNavigation _: WKNavigation!, withError error: Error) { + let webKitError = error as NSError + if webKitError.code == NSURLErrorNotConnectedToInternet { + // Report the error inside the web view. + let localizedErrorMessage = NSLocalizedString("An error occurred:", comment: "") + + let message = "\(localizedErrorMessage) \(error.localizedDescription)" + let errorHTML = + "
\(message)
" + + webView.loadHTMLString(errorHTML, baseURL: nil) + } + } +} diff --git a/PostHogTests/PostHogAutocaptureEventTrackerSpec.swift b/PostHogTests/PostHogAutocaptureEventTrackerSpec.swift new file mode 100644 index 000000000..6cc925354 --- /dev/null +++ b/PostHogTests/PostHogAutocaptureEventTrackerSpec.swift @@ -0,0 +1,83 @@ +// +// PostHogAutocaptureEventTrackerSpec.swift +// PostHog +// +// Created by Yiannis Josephides on 31/10/2024. +// + +#if os(iOS) + import Foundation + import Nimble + @testable import PostHog + import Quick + import UIKit + + class PostHogAutocaptureEventTrackerSpec: QuickSpec { + override func spec() { + context("when generating event data") { + it("should correctly create event data for UIView") { @MainActor in + let view = UIView() + let eventData = view.eventData! + + expect(eventData.targetClass).to(equal("UIView")) + expect(eventData.viewHierarchy.count).to(equal(1)) + } + + it("should correctly create event data for UIView with view hierarchy") { @MainActor in + let superview = UIView() + let button = UIButton() + superview.addSubview(button) + let eventData = button.eventData! + + expect(eventData.targetClass).to(equal("UIButton")) + expect(eventData.viewHierarchy.count).to(equal(2)) + expect(eventData.screenName).to(beNil()) + } + + it("when sanitizing text for autocapture text should be trimmed") { @MainActor in + let button = UIButton() + button.setTitle(" Hello, world! 🌎 ", for: .normal) + let eventData = button.eventData! + + expect(eventData.value).to(equal("Hello, world! 🌎")) + } + + it("when sanitizing text for autocapture text should be limited") { @MainActor in + let button = UIButton() + button.setTitle(String(repeating: "b", count: 300), for: .normal) + let eventData = button.eventData! + + expect(eventData.value).to(equal(String(repeating: "b", count: 255) + "...")) + } + } + + context("shouldTrack method") { + it("should not track hidden views") { @MainActor in + let view = UIView() + view.isHidden = true + expect(view.eventData).to(beNil()) + } + + it("should not track views without user interaction enabled") { @MainActor in + let view = UIView() + view.isUserInteractionEnabled = false + expect(view.eventData).to(beNil()) + } + + it("should not track views marked as ph-no-capture") { @MainActor in + let view = UIView() + view.accessibilityIdentifier = "ph-no-capture" // example condition to make `isNoCapture` return true + expect(view.eventData).to(beNil()) + } + + it("should track views that are visible and interactive") { @MainActor in + let view = UIView() + view.isHidden = false + view.isUserInteractionEnabled = true + expect(view.eventData).toNot(beNil()) + } + } + } + } + +#endif diff --git a/PostHogTests/PostHogAutocaptureIntegrationSpec.swift b/PostHogTests/PostHogAutocaptureIntegrationSpec.swift new file mode 100644 index 000000000..8c64fba4f --- /dev/null +++ b/PostHogTests/PostHogAutocaptureIntegrationSpec.swift @@ -0,0 +1,131 @@ +// +// PostHogAutocaptureIntegrationSpec.swift +// PostHog +// +// Created by Yiannis Josephides on 31/10/2024. +// + +import Foundation +import Nimble +@testable import PostHog +import Quick + +#if os(iOS) + class PostHogAutocaptureIntegrationSpec: QuickSpec { + override func spec() { + var server: MockPostHogServer! + var integration: PostHogAutocaptureIntegration! + + beforeEach { + let config = PostHogConfig(apiKey: "123", host: "http://localhost:9001") + config.captureElementInteractions = true + config.flushIntervalSeconds = 0.2 + config.maxBatchSize = 1 + + server = MockPostHogServer() + server.start() + + PostHogSDK.shared.setup(config) + + integration = PostHogAutocaptureIntegration(config) + integration.start() + } + + afterEach { + server.stop() + server = nil + integration.stop() + PostHogSessionManager.shared.endSession {} + PostHogSDK.shared.close() + deleteSafely(applicationSupportDirectoryURL()) + } + + context("when initialized") { + it("should set the eventProcessor to itself on start") { + integration.start() + expect(PostHogAutocaptureEventTracker.eventProcessor).to(beIdenticalTo(integration)) + } + + it("should clear the eventProcessor on stop") { + integration.start() + integration.stop() + expect(PostHogAutocaptureEventTracker.eventProcessor).to(beNil()) + } + } + + context("processing events") { + it("should process an event") { + let event = createTestEventData() + integration.process(source: .actionMethod(description: "buttonPress"), event: event) + integration.process(source: .actionMethod(description: "buttonPress"), event: event) + + let events = getBatchedEvents(server) + + expect(events.count).to(equal(1)) + } + + it("should respect shouldProcess based on configuration") { + let event = createTestEventData() + + server.start(batchCount: 2) + + integration.process(source: .actionMethod(description: "action"), event: event) + integration.process(source: .actionMethod(description: "action"), event: event) + integration.process(source: .gestureRecognizer(description: "gesture1"), event: event) + + let events = getBatchedEvents(server) + + expect(events.count).to(equal(2)) + } + + it("should debounce events if debounceInterval is greater than 0") { + let debouncedEvent = createTestEventData(debounceInterval: 0.2) + + integration.process(source: .actionMethod(description: "action"), event: debouncedEvent) + integration.process(source: .actionMethod(description: "action"), event: debouncedEvent) + integration.process(source: .actionMethod(description: "action"), event: debouncedEvent) + integration.process(source: .actionMethod(description: "action"), event: debouncedEvent) + integration.process(source: .actionMethod(description: "action"), event: debouncedEvent) + integration.process(source: .actionMethod(description: "action"), event: debouncedEvent) + + PostHogSDK.shared.flush() + + let debouncedEvents = getBatchedEvents(server) + + expect(debouncedEvents.count).to(equal(1)) + + server.start(batchCount: 6) + let event = createTestEventData() + integration.process(source: .actionMethod(description: "action"), event: event) + integration.process(source: .actionMethod(description: "action"), event: event) + integration.process(source: .actionMethod(description: "action"), event: event) + integration.process(source: .actionMethod(description: "action"), event: event) + integration.process(source: .actionMethod(description: "action"), event: event) + integration.process(source: .actionMethod(description: "action"), event: event) + + PostHogSDK.shared.flush() + + let events = getBatchedEvents(server) + + expect(events.count).to(equal(6)) + } + } + } + } + + // Helper function to create test event data + private func createTestEventData(debounceInterval: TimeInterval = 0) -> PostHogAutocaptureEventTracker.EventData { + PostHogAutocaptureEventTracker.EventData( + touchCoordinates: nil, + value: nil, + screenName: "TestScreen", + viewHierarchy: [ + .init(text: "Test Button", targetClass: "UIButton", index: 0, subviewCount: 0), + ], + targetClass: "UIButton", + accessibilityLabel: nil, + accessibilityIdentifier: nil, + debounceInterval: debounceInterval + ) + } +#endif diff --git a/PostHogTests/PostHogConfigTest.swift b/PostHogTests/PostHogConfigTest.swift index 10ff27d9f..78a28bc4d 100644 --- a/PostHogTests/PostHogConfigTest.swift +++ b/PostHogTests/PostHogConfigTest.swift @@ -40,5 +40,22 @@ class PostHogConfigTest: QuickSpec { expect(config.host) == URL(string: "localhost:9000")! } + + #if os(iOS) + context("when initialized with default values for captureElementInteractions") { + it("should enable autocapture by default") { + let sut = PostHogConfig(apiKey: "123") + expect(sut.captureElementInteractions).to(beFalse()) + } + } + + context("when customized") { + it("should allow disabling autocapture") { + let config = PostHogConfig(apiKey: "123") + config.captureElementInteractions = false + expect(config.captureElementInteractions).to(beFalse()) + } + } + #endif } } diff --git a/PostHogTests/PostHogSDKTest.swift b/PostHogTests/PostHogSDKTest.swift index 79f8f9d02..b06fd768d 100644 --- a/PostHogTests/PostHogSDKTest.swift +++ b/PostHogTests/PostHogSDKTest.swift @@ -896,6 +896,26 @@ class PostHogSDKTest: QuickSpec { expect(event[0].event).to(equal("$feature_flag_called")) expect(event[1].event).to(equal("$feature_flag_called")) } + + #if os(iOS) + context("autocapture") { + it("isAutocaptureActive() should be false if disabled by config") { + let config = PostHogConfig(apiKey: "1234") + config.captureElementInteractions = false + let sut = PostHogSDK.with(config) + + expect(sut.isAutocaptureActive()).to(beFalse()) + } + + it("isAutocaptureActive() should be false if SDK is not enabled") { + let config = PostHogConfig(apiKey: "1234") + config.captureElementInteractions = true + let sut = PostHogSDK.with(config) + sut.close() + expect(sut.isAutocaptureActive()).to(beFalse()) + } + } + #endif } }