├── .gitignore
├── ImpressionKitExample
├── ImpressionKitExample
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── SwiftUIScrollViewDemoView.swift
│ ├── AppDelegate.swift
│ ├── SwiftUIListDemoView.swift
│ ├── Info.plist
│ ├── Base.lproj
│ │ └── LaunchScreen.storyboard
│ ├── ScrollViewDemoViewController.swift
│ ├── TableViewDemoViewController.swift
│ ├── CollectionViewDemoViewController.swift
│ ├── CollectionViewDemo2ViewController.swift
│ └── HomeViewController.swift
├── ImpressionKitExample.xcodeproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ ├── xcshareddata
│ │ └── xcschemes
│ │ │ └── ImpressionKitExample.xcscheme
│ └── project.pbxproj
├── ImpressionKitExample.xcworkspace
│ ├── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── contents.xcworkspacedata
├── Podfile
└── Podfile.lock
├── Podfile
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── contents.xcworkspacedata
├── ImpressionKit.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── xcshareddata
│ └── xcschemes
│ │ └── ImpressionKit.xcscheme
└── project.pbxproj
├── ImpressionKit.xcworkspace
├── contents.xcworkspacedata
└── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Podfile.lock
├── ImpressionKit
├── ImpressionKit.h
├── Info.plist
├── ImpressionKitDebug.swift
├── ImpressionKit+ViewModifier.swift
├── ImpressionGroup.swift
├── UIViewPropertyExtension.swift
└── UIViewFunctionExtension.swift
├── Package.resolved
├── ImpressionKit.podspec
├── LICENSE
├── Package.swift
├── README.zh-Hans.md
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | Pods
3 | xcuserdata
4 | Build
5 |
--------------------------------------------------------------------------------
/ImpressionKitExample/ImpressionKitExample/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Podfile:
--------------------------------------------------------------------------------
1 | source 'https://github.com/CocoaPods/Specs.git'
2 | platform :ios, '12.0'
3 |
4 | target 'ImpressionKit' do
5 | use_frameworks!
6 | pod 'EasySwiftHook', "~> 3.5.3"
7 |
8 | end
9 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ImpressionKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ImpressionKitExample/ImpressionKitExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ImpressionKitExample/ImpressionKitExample/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/ImpressionKit.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ImpressionKit.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ImpressionKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ImpressionKitExample/ImpressionKitExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ImpressionKitExample/ImpressionKitExample.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ImpressionKitExample/ImpressionKitExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - EasySwiftHook (3.5.3):
3 | - libffi_apple (~> 3.4.7)
4 | - libffi_apple (3.4.7)
5 |
6 | DEPENDENCIES:
7 | - EasySwiftHook (~> 3.5.3)
8 |
9 | SPEC REPOS:
10 | https://github.com/CocoaPods/Specs.git:
11 | - EasySwiftHook
12 | - libffi_apple
13 |
14 | SPEC CHECKSUMS:
15 | EasySwiftHook: 60c36c9b65f7e177db97adb8e634a7ce6753f768
16 | libffi_apple: b124fcff981c2cc81100c416b87473f9544bad8b
17 |
18 | PODFILE CHECKSUM: 8b4db16f36f0e83b9b53ce6262fbd96c3f6a2d1d
19 |
20 | COCOAPODS: 1.16.2
21 |
--------------------------------------------------------------------------------
/ImpressionKit/ImpressionKit.h:
--------------------------------------------------------------------------------
1 | //
2 | // ImpressionKit.h
3 | // ImpressionKit
4 | //
5 | // Created by Yanni Wang on 30/5/21.
6 | //
7 |
8 | #import
9 |
10 | //! Project version number for ImpressionKit.
11 | FOUNDATION_EXPORT double ImpressionKitVersionNumber;
12 |
13 | //! Project version string for ImpressionKit.
14 | FOUNDATION_EXPORT const unsigned char ImpressionKitVersionString[];
15 |
16 | // In this header, you should import all the public headers of your framework using statements like #import
17 |
18 |
19 |
--------------------------------------------------------------------------------
/ImpressionKitExample/Podfile:
--------------------------------------------------------------------------------
1 | source 'https://github.com/CocoaPods/Specs.git'
2 | platform :ios, '12.0'
3 |
4 | target 'ImpressionKitExample' do
5 | use_frameworks!
6 |
7 | pod 'ImpressionKit', :path=>'../'
8 | pod 'GDPerformanceView-Swift', '~> 2.0.3'
9 | pod 'CHTCollectionViewWaterfallLayout', '~> 0.9.7'
10 | pod 'Eureka'
11 |
12 | end
13 |
14 | post_install do |installer|
15 | installer.generated_projects.each do |project|
16 | project.targets.each do |target|
17 | target.build_configurations.each do |config|
18 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
19 | end
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "libffi_iOS",
6 | "repositoryURL": "https://github.com/623637646/libffi.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "c006945112296b8dc4c47bf70cd3ad5fc31488cf",
10 | "version": "3.3.6-iOS"
11 | }
12 | },
13 | {
14 | "package": "SwiftHook",
15 | "repositoryURL": "https://github.com/623637646/SwiftHook.git",
16 | "state": {
17 | "branch": null,
18 | "revision": "2c03fa626fd042b5244e75fcf5ab42d6937734a2",
19 | "version": "3.3.0"
20 | }
21 | }
22 | ]
23 | },
24 | "version": 1
25 | }
26 |
--------------------------------------------------------------------------------
/ImpressionKit/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 |
22 |
23 |
--------------------------------------------------------------------------------
/ImpressionKit.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 |
3 | s.name = "ImpressionKit"
4 | s.version = "3.7.7"
5 | s.summary = "A tool to detect impression events for UIView (exposure of UIView) in iOS."
6 |
7 | s.description = <<-DESC
8 | This is a user behavior tracking (UBT) tool to analyze impression events for UIView (exposure of UIView) in iOS.
9 | DESC
10 |
11 | s.homepage = "https://github.com/623637646/ImpressionKit"
12 |
13 | s.license = "MIT"
14 |
15 | s.author = { "Yanni Wang" => "wy19900729@gmail.com" }
16 |
17 | s.platform = :ios, "12.0"
18 |
19 | s.swift_versions = "5"
20 |
21 | s.source = { :git => "https://github.com/623637646/ImpressionKit.git", :tag => "#{s.version}" }
22 |
23 | s.source_files = "ImpressionKit/**/*.{swift}"
24 |
25 | s.dependency "EasySwiftHook", "~> 3.5.3"
26 |
27 | end
28 |
--------------------------------------------------------------------------------
/ImpressionKitExample/ImpressionKitExample/SwiftUIScrollViewDemoView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftUIScrollViewDemoView.swift
3 | // ImpressionKitExample
4 | //
5 | // Created by PangMo5 on 2021/07/22.
6 | //
7 |
8 | import Foundation
9 | import ImpressionKit
10 | import SwiftUI
11 |
12 | @available(iOS 13.0, *)
13 | struct SwiftUIScrollViewDemoView: View {
14 | var body: some View {
15 | ScrollView{
16 | ForEach(0 ..< 100) { _ in
17 | CellView()
18 | }
19 | }
20 | }
21 | }
22 |
23 | @available(iOS 13.0, *)
24 | extension SwiftUIScrollViewDemoView {
25 | struct CellView: View {
26 | @State var state: UIView.ImpressionState = .unknown
27 | var body: some View {
28 | (state.isImpressed ? Color.green : Color.red)
29 | .frame(height: 44)
30 | .detectImpression { (state) in
31 | self.state = state
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 王氩
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.8
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "ImpressionKit",
8 | platforms: [.iOS(.v12)],
9 | products: [
10 | // Products define the executables and libraries a package produces, and make them visible to other packages.
11 | .library(
12 | name: "ImpressionKit",
13 | targets: ["ImpressionKit"]),
14 | ],
15 | dependencies: [
16 | // Dependencies declare other packages that this package depends on.
17 | .package(url: "https://github.com/623637646/SwiftHook.git", "3.5.3"..<"4.0.0")
18 | ],
19 | targets: [
20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
21 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
22 |
23 | // Source Code
24 | .target(
25 | name: "ImpressionKit",
26 | dependencies: [.product(name: "SwiftHook", package: "SwiftHook")],
27 | path: "ImpressionKit",
28 | exclude: ["ImpressionKit.h", "Info.plist"]),
29 | ]
30 | )
31 |
--------------------------------------------------------------------------------
/ImpressionKitExample/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - CHTCollectionViewWaterfallLayout (0.9.10):
3 | - CHTCollectionViewWaterfallLayout/Swift (= 0.9.10)
4 | - CHTCollectionViewWaterfallLayout/Swift (0.9.10)
5 | - EasySwiftHook (3.5.3):
6 | - libffi_apple (~> 3.4.7)
7 | - Eureka (5.5.0)
8 | - GDPerformanceView-Swift (2.0.3)
9 | - ImpressionKit (3.7.7):
10 | - EasySwiftHook (~> 3.5.3)
11 | - libffi_apple (3.4.7)
12 |
13 | DEPENDENCIES:
14 | - CHTCollectionViewWaterfallLayout (~> 0.9.7)
15 | - Eureka
16 | - GDPerformanceView-Swift (~> 2.0.3)
17 | - ImpressionKit (from `../`)
18 |
19 | SPEC REPOS:
20 | https://github.com/CocoaPods/Specs.git:
21 | - CHTCollectionViewWaterfallLayout
22 | - EasySwiftHook
23 | - Eureka
24 | - GDPerformanceView-Swift
25 | - libffi_apple
26 |
27 | EXTERNAL SOURCES:
28 | ImpressionKit:
29 | :path: "../"
30 |
31 | SPEC CHECKSUMS:
32 | CHTCollectionViewWaterfallLayout: b989a459788d7657ac010e03bc0f7eedca95a420
33 | EasySwiftHook: 60c36c9b65f7e177db97adb8e634a7ce6753f768
34 | Eureka: 1c18c7fcd8f772cc2ca42d6be36292dffa77eecb
35 | GDPerformanceView-Swift: ef3d05deb103e472a5494d3f60aeca5a58b88a7d
36 | ImpressionKit: 717c5ecb48b576ffb0c8f0eb743a6925189e8af5
37 | libffi_apple: b124fcff981c2cc81100c416b87473f9544bad8b
38 |
39 | PODFILE CHECKSUM: c89964fe43fcd7b8702d18e00e485d43e7f9bd63
40 |
41 | COCOAPODS: 1.16.2
42 |
--------------------------------------------------------------------------------
/ImpressionKitExample/ImpressionKitExample/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // ImpressionKitExample
4 | //
5 | // Created by Yanni Wang on 30/5/21.
6 | //
7 |
8 | import UIKit
9 | import GDPerformanceView_Swift
10 | import ImpressionKit
11 |
12 | @main
13 | class AppDelegate: UIResponder, UIApplicationDelegate {
14 |
15 | var window: UIWindow?
16 |
17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
18 |
19 | UIView.detectionInterval = HomeViewController.detectionInterval
20 | UIView.durationThreshold = HomeViewController.durationThreshold
21 | UIView.areaRatioThreshold = HomeViewController.areaRatioThreshold
22 | UIView.alphaThreshold = HomeViewController.alphaThreshold
23 | UIView.redetectOptions = HomeViewController.redetectOptions
24 | ImpressionKitDebug.shared.openLogs()
25 |
26 | PerformanceMonitor.shared().start()
27 |
28 | let navigationController = UINavigationController(rootViewController: HomeViewController())
29 | let window = UIWindow(frame: UIScreen.main.bounds)
30 | window.rootViewController = navigationController
31 | self.window = window
32 | window.makeKeyAndVisible()
33 |
34 | return true
35 | }
36 |
37 |
38 | }
39 |
40 |
--------------------------------------------------------------------------------
/ImpressionKitExample/ImpressionKitExample/SwiftUIListDemoView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftUIListDemoView.swift
3 | // ImpressionKitExample
4 | //
5 | // Created by PangMo5 on 2021/07/22.
6 | //
7 |
8 | import Foundation
9 | import ImpressionKit
10 | import SwiftUI
11 |
12 | @available(iOS 13.0, *)
13 | final class SwiftUIListDemoViewModel: ObservableObject {
14 | lazy var group = ImpressionGroup.init { [weak self] (_, index: Int, _, state) in
15 | if state.isImpressed {
16 | print("impressed index: \(index)")
17 | }
18 | self?.list[index].1 = state
19 | }
20 |
21 | @Published
22 | var list = (0 ..< 100).map { index in (index, UIView.ImpressionState.unknown) }
23 | }
24 |
25 | @available(iOS 13.0, *)
26 | struct SwiftUIListDemoView: View {
27 | @ObservedObject
28 | var viewModel = SwiftUIListDemoViewModel()
29 |
30 | var body: some View {
31 | List(viewModel.list, id: \.0) { index, state in
32 | CellView(index: index)
33 | .frame(height: 100)
34 | .background(state.isImpressed ? Color.green : Color.red)
35 | .detectImpression(group: viewModel.group, index: index)
36 | }
37 | }
38 | }
39 |
40 | @available(iOS 13.0, *)
41 | extension SwiftUIListDemoView {
42 | struct CellView: View {
43 | let index: Int
44 |
45 | var body: some View {
46 | Text(String(index))
47 | .frame(maxWidth: .infinity, alignment: .center)
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/ImpressionKitExample/ImpressionKitExample/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSupportsIndirectInputEvents
24 |
25 | UILaunchStoryboardName
26 | LaunchScreen
27 | UIRequiredDeviceCapabilities
28 |
29 | armv7
30 |
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 |
37 | UISupportedInterfaceOrientations~ipad
38 |
39 | UIInterfaceOrientationPortrait
40 | UIInterfaceOrientationPortraitUpsideDown
41 | UIInterfaceOrientationLandscapeLeft
42 | UIInterfaceOrientationLandscapeRight
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/ImpressionKitExample/ImpressionKitExample/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/ImpressionKitExample/ImpressionKitExample/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/ImpressionKit.xcodeproj/xcshareddata/xcschemes/ImpressionKit.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
57 |
58 |
59 |
60 |
62 |
63 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/ImpressionKit/ImpressionKitDebug.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Debug.swift
3 | // ImpressionKit
4 | //
5 | // Created by Yanni Wang on 31/5/21.
6 | //
7 |
8 | #if DEBUG
9 | import UIKit
10 | #if SWIFT_PACKAGE
11 | import SwiftHook
12 | #else
13 | import EasySwiftHook
14 | #endif
15 |
16 | public class ImpressionKitDebug {
17 |
18 | public static let shared = ImpressionKitDebug()
19 |
20 | internal var timerCount: UInt = 0
21 | internal var loggedTimerCount: UInt?
22 |
23 | private var viewControllerCount: UInt = 0
24 | private var loggedViewControllerCount: UInt?
25 |
26 | private var viewCount: UInt = 0
27 | private var loggedViewCount: UInt?
28 |
29 | private init() {}
30 | private static let dispatchOnce: () = {
31 | ImpressionKitDebug.shared.hook()
32 | ImpressionKitDebug.shared.timer()
33 | }()
34 |
35 | public func openLogs() {
36 | _ = ImpressionKitDebug.dispatchOnce
37 | }
38 |
39 | private func timer() {
40 | let timer = Timer.init(timeInterval: 0.1, repeats: true, block: { (_) in
41 | let timerCount = ImpressionKitDebug.shared.timerCount
42 | if let loggedTimerCount = ImpressionKitDebug.shared.loggedTimerCount,
43 | loggedTimerCount == timerCount {} else {
44 | print("[ImpressionKit] Timer: \(timerCount)")
45 | ImpressionKitDebug.shared.loggedTimerCount = timerCount
46 | }
47 |
48 | let viewControllerCount = ImpressionKitDebug.shared.viewControllerCount
49 | if let loggedViewControllerCount = ImpressionKitDebug.shared.loggedViewControllerCount,
50 | loggedViewControllerCount == viewControllerCount {} else {
51 | print("[ImpressionKit] UIViewController: \(viewControllerCount)")
52 | ImpressionKitDebug.shared.loggedViewControllerCount = viewControllerCount
53 | }
54 |
55 | let viewCount = ImpressionKitDebug.shared.viewCount
56 | if let loggedViewCount = ImpressionKitDebug.shared.loggedViewCount,
57 | loggedViewCount == viewCount {} else {
58 | print("[ImpressionKit] UIView: \(viewCount)")
59 | ImpressionKitDebug.shared.loggedViewCount = viewCount
60 | }
61 | })
62 | RunLoop.main.add(timer, forMode: .common)
63 | }
64 |
65 | private func hook() {
66 | try! hookAfter(targetClass: UIResponder.self, selector: #selector(UIResponder.init)) { object, selector in
67 | if object is UIViewController {
68 | ImpressionKitDebug.shared.viewControllerCount += 1
69 | } else if object is UIView {
70 | ImpressionKitDebug.shared.viewCount += 1
71 | }
72 | }
73 |
74 | try! hookDeallocBefore(targetClass: UIResponder.self) { object in
75 | if object is UIViewController {
76 | ImpressionKitDebug.shared.viewControllerCount -= 1
77 | } else if object is UIView {
78 | ImpressionKitDebug.shared.viewCount -= 1
79 | }
80 | }
81 | }
82 |
83 | }
84 |
85 | #endif
86 |
--------------------------------------------------------------------------------
/ImpressionKitExample/ImpressionKitExample.xcodeproj/xcshareddata/xcschemes/ImpressionKitExample.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/ImpressionKit/ImpressionKit+ViewModifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImpressionKit+ViewModifier.swift
3 | // ImpressionKit
4 | //
5 | // Created by PangMo5 on 2021/07/22.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 | #if canImport(SwiftUI)
11 | import SwiftUI
12 | #endif
13 |
14 | @available(iOS 13.0, *)
15 | private struct ImpressionView: UIViewRepresentable {
16 | let isForGroup: Bool
17 | let detectionInterval: Float?
18 | let durationThreshold: Float?
19 | let areaRatioThreshold: Float?
20 | let redetectOptions: UIView.Redetect?
21 | let onCreated: ((UIView) -> Void)?
22 | let onChanged: ((UIView.ImpressionState) -> Void)?
23 |
24 | func makeUIView(context: UIViewRepresentableContext) -> UIView {
25 | let view = UIView(frame: .zero)
26 | view.backgroundColor = .clear
27 | view.detectionInterval = detectionInterval
28 | view.durationThreshold = durationThreshold
29 | view.areaRatioThreshold = areaRatioThreshold
30 | view.redetectOptions = redetectOptions
31 | if !isForGroup {
32 | view.detectImpression { _, state in
33 | onChanged?(state)
34 | }
35 | }
36 | onCreated?(view)
37 | return view
38 | }
39 |
40 | func updateUIView(_ uiView: UIView, context: Context) {}
41 | }
42 |
43 | @available(iOS 13.0, *)
44 | private struct ImpressionTrackableModifier: ViewModifier {
45 | let isForGroup: Bool
46 | let detectionInterval: Float?
47 | let durationThreshold: Float?
48 | let areaRatioThreshold: Float?
49 | let redetectOptions: UIView.Redetect?
50 | let onCreated: ((UIView) -> Void)?
51 | let onChanged: ((UIView.ImpressionState) -> Void)?
52 |
53 | func body(content: Content) -> some View {
54 | content
55 | .overlay(ImpressionView(isForGroup: isForGroup,
56 | detectionInterval: detectionInterval,
57 | durationThreshold: durationThreshold,
58 | areaRatioThreshold: areaRatioThreshold,
59 | redetectOptions: redetectOptions,
60 | onCreated: onCreated,
61 | onChanged: onChanged)
62 | .allowsHitTesting(false))
63 | }
64 | }
65 |
66 | @available(iOS 13.0, *)
67 | public extension View {
68 | func detectImpression(detectionInterval: Float? = nil,
69 | durationThreshold: Float? = nil,
70 | areaRatioThreshold: Float? = nil,
71 | redetectOptions: UIView.Redetect? = nil,
72 | onChanged: @escaping (UIView.ImpressionState) -> Void) -> some View
73 | {
74 | modifier(ImpressionTrackableModifier(isForGroup: false,
75 | detectionInterval: detectionInterval,
76 | durationThreshold: durationThreshold,
77 | areaRatioThreshold: areaRatioThreshold,
78 | redetectOptions: redetectOptions,
79 | onCreated: nil,
80 | onChanged: onChanged))
81 | }
82 |
83 | func detectImpression(group: ImpressionGroup,
84 | index: T) -> some View
85 | {
86 | modifier(ImpressionTrackableModifier(isForGroup: true,
87 | detectionInterval: group.detectionInterval,
88 | durationThreshold: group.durationThreshold,
89 | areaRatioThreshold: group.areaRatioThreshold,
90 | redetectOptions: group.redetectOptions,
91 | onCreated: { view in
92 | group.bind(view: view, index: index)
93 | },
94 | onChanged: nil))
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/README.zh-Hans.md:
--------------------------------------------------------------------------------
1 | # ImpressionKit
2 |
3 | 这是一个用户行为追踪(UBT)工具。可以方便地检测 UIView 的曝光事件。
4 |
5 | 
6 |
7 | 原理:用 [SwiftHook](https://github.com/623637646/SwiftHook) Hook UIView的`didMoveToWindow`方法,定时检测此UIView是否在屏幕上。
8 |
9 | # 怎么使用
10 |
11 | ### 主要的 API
12 |
13 | 非常简单.
14 |
15 | ```swift
16 |
17 | // UIKit
18 |
19 | UIView().detectImpression { (view, state) in
20 | if state.isImpressed {
21 | print("This view is impressed to users.")
22 | }
23 | }
24 |
25 | // SwiftUI
26 |
27 | Color.red.detectImpression { state in
28 | if state.isImpressed {
29 | print("This view is impressed to users.")
30 | }
31 | }
32 | ```
33 |
34 | 如果是在 UICollectionView,UITableView 或者其他可复用的视图中,请使用`ImpressionGroup`。
35 |
36 | ```swift
37 | // UIKit
38 |
39 | var group = ImpressionGroup.init {(_, index: IndexPath, view, state) in
40 | if state.isImpressed {
41 | print("impressed index: \(index.row)")
42 | }
43 | }
44 |
45 | ...
46 |
47 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
48 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
49 | self.group.bind(view: cell, index: indexPath)
50 | return cell
51 | }
52 |
53 | // SwiftUI
54 |
55 | var group = ImpressionGroup.init { (_, index: Int, _, state) in
56 | if state.isImpressed {
57 | print("impressed index: \(index)")
58 | }
59 | }
60 |
61 | var body: some View {
62 | List(0 ..< 100) { index in
63 | CellView(index: index)
64 | .frame(height: 100)
65 | .detectImpression(group: group, index: index)
66 | }
67 | }
68 | ```
69 |
70 | ### 更多 API
71 |
72 | 更改检测间隔 (秒). 更小的 `检测间隔` 代表更高的精确性和更高的CPU消耗。
73 |
74 | ```swift
75 | UIView.detectionInterval = 0.1 // 应用于所有 view。
76 | UIView().detectionInterval = 0.1 // 应用于特定 view. 如果为nil,则使用 `UIView.detectionInterval`。
77 | ImpressionGroup().detectionInterval = 0.1 // 应用于特定 group. 如果为nil,则使用 `UIView.detectionInterval`。
78 | ```
79 |
80 | 更改 view 在屏幕上的持续时间的阈值 (秒)。 如果 view 在屏幕上的持续时间超过此阈值则可能会触发曝光。
81 |
82 | ```swift
83 | UIView.durationThreshold = 2 // 应用于所有 view。
84 | UIView().durationThreshold = 2 // 应用于特定 view. 如果为nil,则使用 `UIView.durationThreshold`。
85 | ImpressionGroup().durationThreshold = 2 // 应用于特定 group. 如果为nil,则使用 `UIView.durationThreshold`。
86 | ```
87 |
88 | 更改 view 在屏幕上的面积比例的阈值(从 0 到 1)。view 在屏幕上的面积的百分比超过此阈值则可能会触发曝光。
89 |
90 | ```swift
91 | UIView.areaRatioThreshold = 0.4 // 应用于所有 view。
92 | UIView().areaRatioThreshold = 0.4 // 应用于特定 view. 如果为nil,则使用 `UIView.areaRatioThreshold`。
93 | ImpressionGroup().areaRatioThreshold = 0.4 // 应用于特定 group. 如果为nil,则使用 `UIView.areaRatioThreshold` 。
94 | ```
95 |
96 | 更改 view 透明度的阈值(从 0 到 1)。view 透明度超过此阈值则可能会触发曝光。
97 |
98 | ```swift
99 | UIView.alphaThreshold = 0.4 // 应用于所有 view。
100 | UIView().alphaThreshold = 0.4 // 应用于特定 view. 如果为nil,则使用 `UIView.alphaThreshold`。
101 | ImpressionGroup().alphaThreshold = 0.4 // 应用于特定 group. 如果为nil,则使用 `UIView.alphaThreshold` 。
102 | ```
103 |
104 | 在某些情况下重新触发曝光事件。
105 |
106 | ```swift
107 | // 当 view 离开屏幕 (页面不变,只是 view 没有显示)时,重新触发曝光。
108 | public static let leftScreen = Redetect(rawValue: 1 << 0)
109 |
110 | // 当 view 所在 UIViewController 消失时,重新触发曝光。
111 | public static let viewControllerDidDisappear = Redetect(rawValue: 1 << 1)
112 |
113 | // 当 App 进入后台时,重新触发曝光。
114 | public static let didEnterBackground = Redetect(rawValue: 1 << 2)
115 |
116 | // 当 App 将要进入非活跃状态时,重新触发曝光。
117 | public static let willResignActive = Redetect(rawValue: 1 << 3)
118 | ```
119 |
120 | ```swift
121 | UIView.redetectOptions = [.leftScreen, .viewControllerDidDisappear, .didEnterBackground, .willResignActive] // 应用于所有 view。
122 | UIView().redetectOptions = [.leftScreen, .viewControllerDidDisappear, .didEnterBackground, .willResignActive] // 应用于特定 view. 如果为nil,则使用`UIView.redetectOptions`。
123 | ImpressionGroup().redetectOptions = [.leftScreen, .viewControllerDidDisappear, .didEnterBackground, .willResignActive] // 应用于特定 group. 如果为nil,则使用 `UIView.redetectOptions`。
124 | ```
125 |
126 | 查看Demo获取更多详情。
127 |
128 | # How to integrate ImpressionKit
129 |
130 | [cocoapods](https://cocoapods.org/).
131 |
132 | ```
133 | pod 'ImpressionKit'
134 | ```
135 |
136 | 或者使用 Swift Package Manager。 3.1.0 版本之后,SPM被支持。
137 |
138 | # Requirements
139 |
140 | - iOS 12.0+ (UIKit)
141 | - iOS 13.0+ (SwiftUI)
142 | - Xcode 15.1+
143 |
--------------------------------------------------------------------------------
/ImpressionKitExample/ImpressionKitExample/ScrollViewDemoViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScrollViewDemoViewController.swift
3 | // ImpressionKitExample
4 | //
5 | // Created by Yanni Wang on 31/5/21.
6 | //
7 |
8 | import UIKit
9 | import ImpressionKit
10 |
11 | class ScrollViewDemoViewController: UIViewController {
12 |
13 | private static let column = 4
14 |
15 | private let views = {
16 | return [Int](0...99).map { (index) -> CellView in
17 | return CellView.init(index: UInt(index))
18 | }
19 | }()
20 |
21 | private let scrollView = UIScrollView.init()
22 |
23 | override func viewDidLoad() {
24 | super.viewDidLoad()
25 | self.view.backgroundColor = .white
26 | self.title = "UIScrollView"
27 |
28 | self.navigationItem.rightBarButtonItems = [
29 | UIBarButtonItem.init(title: "push", style: .plain, target: self, action: #selector(pushNextPage)),
30 | UIBarButtonItem.init(title: "present", style: .plain, target: self, action: #selector(presentNextPage)),
31 | UIBarButtonItem.init(title: "redetect", style: .plain, target: self, action: #selector(redetect)),
32 | ]
33 |
34 | self.scrollView.frame = self.view.bounds
35 | self.view.addSubview(self.scrollView)
36 |
37 | var bottoms = [CGFloat].init(repeating: 0, count: ScrollViewDemoViewController.column)
38 | for cell in self.views {
39 | let y = bottoms.min()!
40 | let columnIndex = bottoms.firstIndex(of: y)!
41 | let width = self.scrollView.frame.width / CGFloat(ScrollViewDemoViewController.column)
42 | let height = width + CGFloat.random(in: 0 ..< width)
43 | let x = CGFloat(columnIndex) * width
44 | bottoms[columnIndex] = y + height
45 | cell.frame = CGRect.init(x: x, y: y, width: width, height: height)
46 | self.scrollView.addSubview(cell)
47 | }
48 | self.scrollView.contentSize = CGSize.init(width: self.scrollView.frame.width, height: bottoms.max()!)
49 | }
50 |
51 | @objc private func redetect() {
52 | self.views.forEach { (cell) in
53 | cell.redetect()
54 | }
55 | }
56 |
57 | @objc private func pushNextPage() {
58 | let nextPage = UIViewController()
59 | nextPage.view.backgroundColor = .white
60 | self.navigationController?.pushViewController(nextPage, animated: true)
61 | }
62 |
63 | @objc private func presentNextPage() {
64 | let nextPage = UIViewController()
65 | nextPage.view.backgroundColor = .white
66 | let backButton = UIButton.init(frame: CGRect.init(x: 0, y: 0, width: 100, height: 40))
67 | backButton.setTitle("back", for: .normal)
68 | backButton.setTitleColor(.black, for: .normal)
69 | backButton.addTarget(self, action: #selector(back), for: .touchUpInside)
70 | backButton.center = CGPoint.init(x: nextPage.view.frame.width / 2, y: nextPage.view.frame.height / 2)
71 | nextPage.view.addSubview(backButton)
72 | self.present(nextPage, animated: true, completion: nil)
73 | }
74 |
75 | @objc func back(){
76 | self.presentedViewController?.dismiss(animated: true, completion: nil)
77 | }
78 | }
79 |
80 | private class CellView: UIView {
81 |
82 | let label = { () -> UILabel in
83 | let label = UILabel.init()
84 | label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
85 | label.font = UIFont.systemFont(ofSize: 12)
86 | label.textColor = .black
87 | label.textAlignment = .center
88 | label.numberOfLines = 0
89 | return label
90 | }()
91 |
92 | let index: UInt
93 |
94 | init(index: UInt) {
95 | self.index = index
96 | super.init(frame: CGRect.zero)
97 | self.layer.borderColor = UIColor.gray.cgColor
98 | self.layer.borderWidth = 0.5
99 | self.alpha = CGFloat(HomeViewController.alphaInDemo)
100 |
101 | self.label.frame = self.bounds
102 | self.addSubview(self.label)
103 | self.updateUI()
104 |
105 | self.detectImpression { (view, state) in
106 | view.updateUI()
107 | if state.isImpressed {
108 | print("impressed index: \(view.index)")
109 | }
110 | }
111 | }
112 |
113 | required init?(coder: NSCoder) {
114 | fatalError("init(coder:) has not been implemented")
115 | }
116 |
117 | func reset() {
118 | self.redetect()
119 | self.updateUI()
120 | }
121 |
122 | private func updateUI() {
123 | self.layer.removeAllAnimations()
124 | switch impressionState {
125 | case .impressed(_, let areaRatio):
126 | self.label.text = String.init(format: "\(self.index)\n\n%0.1f%%", areaRatio * 100)
127 | self.backgroundColor = .green
128 | case .inScreen(_):
129 | self.backgroundColor = .white
130 | UIView.animate(withDuration: TimeInterval(self.durationThreshold ?? UIView.durationThreshold), delay: 0, options: [.curveLinear, .allowUserInteraction], animations: {
131 | self.backgroundColor = .red
132 | }, completion: nil)
133 | default:
134 | self.label.text = "\(self.index)\n\n"
135 | self.backgroundColor = .white
136 | }
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/ImpressionKitExample/ImpressionKitExample/TableViewDemoViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TableViewDemoViewController.swift
3 | // ImpressionKitExample
4 | //
5 | // Created by Yanni Wang on 6/6/21.
6 | //
7 |
8 | import UIKit
9 | import ImpressionKit
10 |
11 | class TableViewDemoViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
12 |
13 | let tableView = { () -> UITableView in
14 | let view = UITableView.init()
15 | view.backgroundColor = .white
16 | view.register(Cell.self, forCellReuseIdentifier: "Cell")
17 | return view
18 | }()
19 |
20 | lazy var group = ImpressionGroup.init {(_, index: IndexPath, view, state) in
21 | if state.isImpressed {
22 | print("impressed index: \(index.row)")
23 | }
24 | if let cell = view.superview as? Cell {
25 | cell.updateUI(state: state)
26 | }
27 | }
28 |
29 | override func viewDidLoad() {
30 | super.viewDidLoad()
31 | self.view.backgroundColor = .white
32 | self.title = "UITableView"
33 |
34 | self.navigationItem.rightBarButtonItems = [
35 | UIBarButtonItem.init(title: "push", style: .plain, target: self, action: #selector(pushNextPage)),
36 | UIBarButtonItem.init(title: "present", style: .plain, target: self, action: #selector(presentNextPage)),
37 | UIBarButtonItem.init(title: "redetect", style: .plain, target: self, action: #selector(redetect)),
38 | ]
39 |
40 | self.tableView.frame = self.view.bounds
41 | self.tableView.dataSource = self
42 | self.tableView.delegate = self
43 | self.view.addSubview(self.tableView)
44 | }
45 |
46 | @objc private func redetect() {
47 | self.group.redetect()
48 | }
49 |
50 | @objc private func pushNextPage() {
51 | let nextPage = UIViewController()
52 | nextPage.view.backgroundColor = .white
53 | self.navigationController?.pushViewController(nextPage, animated: true)
54 | }
55 |
56 | @objc private func presentNextPage() {
57 | let nextPage = UIViewController()
58 | nextPage.view.backgroundColor = .white
59 | let backButton = UIButton.init(frame: CGRect.init(x: 0, y: 0, width: 100, height: 40))
60 | backButton.setTitle("back", for: .normal)
61 | backButton.setTitleColor(.black, for: .normal)
62 | backButton.addTarget(self, action: #selector(back), for: .touchUpInside)
63 | backButton.center = CGPoint.init(x: nextPage.view.frame.width / 2, y: nextPage.view.frame.height / 2)
64 | nextPage.view.addSubview(backButton)
65 | self.present(nextPage, animated: true, completion: nil)
66 | }
67 |
68 | @objc func back(){
69 | self.presentedViewController?.dismiss(animated: true, completion: nil)
70 | }
71 |
72 | // UITableViewDataSource
73 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
74 | return 99
75 | }
76 |
77 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
78 | let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! Cell
79 | cell.contentView.alpha = CGFloat(HomeViewController.alphaInDemo)
80 | self.group.bind(view: cell.contentView, index: indexPath)
81 | cell.index = indexPath.row
82 | cell.updateUI(state: self.group.states[indexPath])
83 | return cell
84 | }
85 |
86 | // UITableViewDelegate
87 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
88 | return 44
89 | }
90 |
91 | }
92 |
93 | private class Cell: UITableViewCell {
94 | private var label = { () -> UILabel in
95 | let view = UILabel.init()
96 | view.font = UIFont.systemFont(ofSize: 12)
97 | view.textColor = .black
98 | view.textAlignment = .center
99 | view.numberOfLines = 0
100 | return view
101 | }()
102 |
103 | var index: Int = -1
104 |
105 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
106 | super.init(style: style, reuseIdentifier: reuseIdentifier)
107 | self.contentView.layer.borderColor = UIColor.gray.cgColor
108 | self.contentView.layer.borderWidth = 0.5
109 | self.contentView.addSubview(label)
110 | }
111 |
112 | required init?(coder: NSCoder) {
113 | fatalError("init(coder:) has not been implemented")
114 | }
115 |
116 | override func layoutSubviews() {
117 | super.layoutSubviews()
118 | self.label.frame = self.contentView.bounds
119 | }
120 |
121 | fileprivate func updateUI(state: UIView.ImpressionState?) {
122 | self.layer.removeAllAnimations()
123 | switch state {
124 | case .impressed(_, let areaRatio):
125 | self.label.text = String.init(format: "\(self.index)\n%0.1f%%", areaRatio * 100)
126 | self.contentView.backgroundColor = .green
127 | case .inScreen(_):
128 | self.contentView.backgroundColor = .white
129 | UIView.animate(withDuration: TimeInterval(self.contentView.durationThreshold ?? UIView.durationThreshold), delay: 0, options: [.curveLinear, .allowUserInteraction], animations: {
130 | self.contentView.backgroundColor = .red
131 | }, completion: nil)
132 | default:
133 | self.label.text = "\(self.index)\n"
134 | self.contentView.backgroundColor = .white
135 | }
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [中文](README.zh-Hans.md)
2 |
3 | # ImpressionKit
4 |
5 | This is a user behavior tracking (UBT) tool to analyze impression events for UIView (exposure of UIView) in iOS.
6 |
7 | 
8 |
9 | How it works: Hook the `didMoveToWindow` method of a UIView by [SwiftHook](https://github.com/623637646/SwiftHook), periodically check the view is on the screen or not.
10 |
11 | # How to use ImpressionKit
12 |
13 | ### Main APIs
14 |
15 | It's quite simple.
16 |
17 |
18 | ```swift
19 |
20 | // UIKit
21 |
22 | UIView().detectImpression { (view, state) in
23 | if state.isImpressed {
24 | print("This view is impressed to users.")
25 | }
26 | }
27 |
28 | // SwiftUI
29 |
30 | Color.red.detectImpression { state in
31 | if state.isImpressed {
32 | print("This view is impressed to users.")
33 | }
34 | }
35 | ```
36 |
37 | Use `ImpressionGroup` for UICollectionView, UITableView, List or other reusable view cases.
38 |
39 | ```swift
40 | // UIKit
41 |
42 | var group = ImpressionGroup.init {(_, index: IndexPath, view, state) in
43 | if state.isImpressed {
44 | print("impressed index: \(index.row)")
45 | }
46 | }
47 |
48 | ...
49 |
50 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
51 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
52 | self.group.bind(view: cell, index: indexPath)
53 | return cell
54 | }
55 |
56 | // SwiftUI
57 |
58 | var group = ImpressionGroup.init { (_, index: Int, _, state) in
59 | if state.isImpressed {
60 | print("impressed index: \(index)")
61 | }
62 | }
63 |
64 | var body: some View {
65 | List(0 ..< 100) { index in
66 | CellView(index: index)
67 | .frame(height: 100)
68 | .detectImpression(group: group, index: index)
69 | }
70 | }
71 | ```
72 |
73 | ### More APIs
74 |
75 | Modify the detection (scan) interval (in seconds). Smaller `detectionInterval` means higher accuracy and higher CPU consumption.
76 |
77 | ```swift
78 | UIView.detectionInterval = 0.1 // apply to all views
79 | UIView().detectionInterval = 0.1 // apply to the specific view. `UIView.detectionInterval` will be used if it's nil.
80 | ImpressionGroup().detectionInterval = 0.1 // apply to the group. `UIView.detectionInterval` will be used if it's nil.
81 | ```
82 |
83 | Modify the threshold (seconds) for the duration of a view on the screen. If the view's duration on the screen exceeds this threshold, it may trigger an impression.
84 |
85 | ```swift
86 | UIView.durationThreshold = 2 // apply to all views
87 | UIView().durationThreshold = 2 // apply to the specific view. `UIView.durationThreshold` will be used if it's nil.
88 | ImpressionGroup().durationThreshold = 2 // apply to the group. `UIView.durationThreshold` will be used if it's nil.
89 | ```
90 |
91 | Modify the threshold (from 0 to 1) for the area ratio of the view on the screen. If the percentage of the view's area on the screen exceeds this threshold, it may trigger an impression.
92 |
93 | ```swift
94 | UIView.areaRatioThreshold = 0.4 // apply to all views
95 | UIView().areaRatioThreshold = 0.4 // apply to the specific view. `UIView.areaRatioThreshold` will be used if it's nil.
96 | ImpressionGroup().areaRatioThreshold = 0.4 // apply to the group. `UIView.areaRatioThreshold` will be used if it's nil.
97 | ```
98 |
99 | Modify the threshold (from 0 to 1) for the view opacity. If the view's opacity exceeds this threshold, it may trigger an impression.
100 |
101 | ```swift
102 | UIView.alphaThreshold = 0.4 // apply to all views
103 | UIView().alphaThreshold = 0.4 // apply to the specific view. `UIView.alphaThreshold` will be used if it's nil.
104 | ImpressionGroup().alphaThreshold = 0.4 // apply to the group. `UIView.alphaThreshold` will be used if it's nil.
105 | ```
106 |
107 | Retrigger the impression event in some situations.
108 |
109 | ```swift
110 | // When a view left from the screen, mark the view to retrigger the impression event when this view come back to the screen.
111 | // Left screen means the view is out of the screen but the UIViewController is still there.
112 | public static let leftScreen = Redetect(rawValue: 1 << 0)
113 |
114 | // When the UIViewController of the view disappear, mark the view to retrigger the impression event when the UIViewController appear again.
115 | public static let viewControllerDidDisappear = Redetect(rawValue: 1 << 1)
116 |
117 | // When the app enter background, mark the view to retrigger the impression event when the app enter foreground.
118 | public static let didEnterBackground = Redetect(rawValue: 1 << 2)
119 |
120 | // When the app will resign active, mark the view to retrigger the impression event when the app become active.
121 | public static let willResignActive = Redetect(rawValue: 1 << 3)
122 | ```
123 |
124 | ```swift
125 | UIView.redetectOptions = [.leftScreen, .viewControllerDidDisappear, .didEnterBackground, .willResignActive] // apply to all views
126 | UIView().redetectOptions = [.leftScreen, .viewControllerDidDisappear, .didEnterBackground, .willResignActive] // apply to the specific view. `UIView.redetectOptions` will be used if it's nil.
127 | ImpressionGroup().redetectOptions = [.leftScreen, .viewControllerDidDisappear, .didEnterBackground, .willResignActive] // apply to the group. `UIView.redetectOptions` will be used if it's nil.
128 | ```
129 |
130 | Refer to the Demo for more details.
131 |
132 | # How to integrate ImpressionKit
133 |
134 | **ImpressionKit** can be integrated by [cocoapods](https://cocoapods.org/).
135 |
136 | ```
137 | pod 'ImpressionKit'
138 | ```
139 |
140 | Or use Swift Package Manager. SPM is supported from 3.1.0.
141 |
142 | # Requirements
143 |
144 | - iOS 12.0+ (UIKit)
145 | - iOS 13.0+ (SwiftUI)
146 | - Xcode 15.1+
147 |
--------------------------------------------------------------------------------
/ImpressionKitExample/ImpressionKitExample/CollectionViewDemoViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CollectionViewDemoViewController.swift
3 | // ImpressionKitExample
4 | //
5 | // Created by Yanni Wang on 31/5/21.
6 | //
7 |
8 | import UIKit
9 | import CHTCollectionViewWaterfallLayout
10 | import ImpressionKit
11 |
12 | class CollectionViewDemoViewController: UIViewController, UICollectionViewDataSource, CHTCollectionViewDelegateWaterfallLayout {
13 |
14 | let collectionView = { () -> UICollectionView in
15 | let layout = CHTCollectionViewWaterfallLayout.init()
16 | layout.columnCount = 4
17 | layout.minimumColumnSpacing = 0
18 | layout.minimumInteritemSpacing = 0
19 |
20 | let view = UICollectionView.init(frame: CGRect.zero, collectionViewLayout: layout)
21 | view.backgroundColor = .white
22 | view.register(Cell.self, forCellWithReuseIdentifier: "Cell")
23 | return view
24 | }()
25 |
26 | lazy var group = ImpressionGroup.init {(_, index: IndexPath, view, state) in
27 | if state.isImpressed {
28 | print("impressed index: \(index.row)")
29 | }
30 | if let cell = view.superview as? Cell {
31 | cell.updateUI(state: state)
32 | }
33 | }
34 |
35 | override func viewDidLoad() {
36 | super.viewDidLoad()
37 | self.view.backgroundColor = .white
38 | self.title = "UICollectionView"
39 |
40 | self.navigationItem.rightBarButtonItems = [
41 | UIBarButtonItem.init(title: "push", style: .plain, target: self, action: #selector(pushNextPage)),
42 | UIBarButtonItem.init(title: "present", style: .plain, target: self, action: #selector(presentNextPage)),
43 | UIBarButtonItem.init(title: "redetect", style: .plain, target: self, action: #selector(redetect)),
44 | ]
45 |
46 | self.collectionView.frame = self.view.bounds
47 | self.collectionView.dataSource = self
48 | self.collectionView.delegate = self
49 | self.view.addSubview(self.collectionView)
50 | }
51 |
52 | @objc private func redetect() {
53 | self.group.redetect()
54 | }
55 |
56 | @objc private func pushNextPage() {
57 | let nextPage = UIViewController()
58 | nextPage.view.backgroundColor = .white
59 | self.navigationController?.pushViewController(nextPage, animated: true)
60 | }
61 |
62 | @objc private func presentNextPage() {
63 | let nextPage = UIViewController()
64 | nextPage.view.backgroundColor = .white
65 | let backButton = UIButton.init(frame: CGRect.init(x: 0, y: 0, width: 100, height: 40))
66 | backButton.setTitle("back", for: .normal)
67 | backButton.setTitleColor(.black, for: .normal)
68 | backButton.addTarget(self, action: #selector(back), for: .touchUpInside)
69 | backButton.center = CGPoint.init(x: nextPage.view.frame.width / 2, y: nextPage.view.frame.height / 2)
70 | nextPage.view.addSubview(backButton)
71 | self.present(nextPage, animated: true, completion: nil)
72 | }
73 |
74 | @objc func back(){
75 | self.presentedViewController?.dismiss(animated: true, completion: nil)
76 | }
77 |
78 | // UICollectionViewDataSource & CHTCollectionViewDelegateWaterfallLayout
79 |
80 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
81 | return 99
82 | }
83 |
84 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
85 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
86 | cell.contentView.alpha = CGFloat(HomeViewController.alphaInDemo)
87 | self.group.bind(view: cell.contentView, index: indexPath)
88 | cell.index = indexPath.row
89 | cell.updateUI(state: self.group.states[indexPath])
90 | return cell
91 | }
92 |
93 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
94 | let width: CGFloat = 100
95 | let height = width + CGFloat.random(in: 0 ..< width)
96 | return CGSize.init(width: width, height: height)
97 | }
98 | }
99 |
100 | private class Cell: UICollectionViewCell {
101 | private var label = { () -> UILabel in
102 | let view = UILabel.init()
103 | view.font = UIFont.systemFont(ofSize: 12)
104 | view.textColor = .black
105 | view.textAlignment = .center
106 | view.numberOfLines = 0
107 | return view
108 | }()
109 |
110 | var index: Int = -1
111 |
112 | override init(frame: CGRect) {
113 | super.init(frame: frame)
114 | self.contentView.layer.borderColor = UIColor.gray.cgColor
115 | self.contentView.layer.borderWidth = 0.5
116 | self.contentView.addSubview(label)
117 | }
118 |
119 | required init?(coder: NSCoder) {
120 | fatalError("init(coder:) has not been implemented")
121 | }
122 |
123 | override func layoutSubviews() {
124 | super.layoutSubviews()
125 | self.label.frame = self.contentView.bounds
126 | }
127 |
128 | fileprivate func updateUI(state: UIView.ImpressionState?) {
129 | self.layer.removeAllAnimations()
130 | switch state {
131 | case .impressed(_, let areaRatio):
132 | self.label.text = String.init(format: "\(self.index)\n\n%0.1f%%", areaRatio * 100)
133 | self.contentView.backgroundColor = .green
134 | case .inScreen(_):
135 | self.contentView.backgroundColor = .white
136 | UIView.animate(withDuration: TimeInterval(self.contentView.durationThreshold ?? UIView.durationThreshold), delay: 0, options: [.curveLinear, .allowUserInteraction], animations: {
137 | self.contentView.backgroundColor = .red
138 | }, completion: nil)
139 | default:
140 | self.label.text = "\(self.index)\n\n"
141 | self.contentView.backgroundColor = .white
142 | }
143 | }
144 | }
145 |
146 |
--------------------------------------------------------------------------------
/ImpressionKitExample/ImpressionKitExample/CollectionViewDemo2ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CollectionViewDemo2ViewController.swift
3 | // ImpressionKitExample
4 | //
5 | // Created by Yanni on 21/1/22.
6 | //
7 |
8 | import UIKit
9 | import CHTCollectionViewWaterfallLayout
10 | import ImpressionKit
11 |
12 | class CollectionViewDemo2ViewController: UIViewController, UICollectionViewDataSource, CHTCollectionViewDelegateWaterfallLayout {
13 |
14 | let collectionView = { () -> UICollectionView in
15 | let layout = CHTCollectionViewWaterfallLayout.init()
16 | layout.columnCount = 4
17 | layout.minimumColumnSpacing = 0
18 | layout.minimumInteritemSpacing = 0
19 |
20 | let view = UICollectionView.init(frame: CGRect.zero, collectionViewLayout: layout)
21 | view.backgroundColor = .white
22 | view.register(Cell.self, forCellWithReuseIdentifier: "Cell")
23 | return view
24 | }()
25 |
26 | lazy var group = ImpressionGroup.init {(_, index: IndexPath, view, state) in
27 | if state.isImpressed {
28 | print("impressed index: \(index.section), \(index.row)")
29 | }
30 | if let cell = view.superview as? Cell {
31 | cell.updateUI(state: state)
32 | }
33 | }
34 |
35 | override func viewDidLoad() {
36 | super.viewDidLoad()
37 | self.view.backgroundColor = .white
38 | self.title = "UICollectionView2"
39 |
40 | self.navigationItem.rightBarButtonItems = [
41 | UIBarButtonItem.init(title: "push", style: .plain, target: self, action: #selector(pushNextPage)),
42 | UIBarButtonItem.init(title: "present", style: .plain, target: self, action: #selector(presentNextPage)),
43 | UIBarButtonItem.init(title: "redetect", style: .plain, target: self, action: #selector(redetect)),
44 | ]
45 |
46 | self.collectionView.frame = self.view.bounds
47 | self.collectionView.dataSource = self
48 | self.collectionView.delegate = self
49 | self.view.addSubview(self.collectionView)
50 | }
51 |
52 | @objc private func redetect() {
53 | self.group.redetect()
54 | }
55 |
56 | @objc private func pushNextPage() {
57 | let nextPage = UIViewController()
58 | nextPage.view.backgroundColor = .white
59 | self.navigationController?.pushViewController(nextPage, animated: true)
60 | }
61 |
62 | @objc private func presentNextPage() {
63 | let nextPage = UIViewController()
64 | nextPage.view.backgroundColor = .white
65 | let backButton = UIButton.init(frame: CGRect.init(x: 0, y: 0, width: 100, height: 40))
66 | backButton.setTitle("back", for: .normal)
67 | backButton.setTitleColor(.black, for: .normal)
68 | backButton.addTarget(self, action: #selector(back), for: .touchUpInside)
69 | backButton.center = CGPoint.init(x: nextPage.view.frame.width / 2, y: nextPage.view.frame.height / 2)
70 | nextPage.view.addSubview(backButton)
71 | self.present(nextPage, animated: true, completion: nil)
72 | }
73 |
74 | @objc func back(){
75 | self.presentedViewController?.dismiss(animated: true, completion: nil)
76 | }
77 |
78 | // UICollectionViewDataSource & CHTCollectionViewDelegateWaterfallLayout
79 |
80 | func numberOfSections(in collectionView: UICollectionView) -> Int {
81 | return 3
82 | }
83 |
84 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
85 | return section == 1 ? 30 : 10
86 | }
87 |
88 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
89 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
90 | cell.contentView.alpha = CGFloat(HomeViewController.alphaInDemo)
91 | self.group.bind(view: cell.contentView, index: indexPath, ignoreDetection: indexPath.section != 1)
92 | cell.index = indexPath.row
93 | cell.updateUI(state: self.group.states[indexPath])
94 | return cell
95 | }
96 |
97 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
98 | let width: CGFloat = 100
99 | let height = width + CGFloat.random(in: 0 ..< width)
100 | return CGSize.init(width: width, height: height)
101 | }
102 | }
103 |
104 | private class Cell: UICollectionViewCell {
105 | private var label = { () -> UILabel in
106 | let view = UILabel.init()
107 | view.font = UIFont.systemFont(ofSize: 12)
108 | view.textColor = .black
109 | view.textAlignment = .center
110 | view.numberOfLines = 0
111 | return view
112 | }()
113 |
114 | var index: Int = -1
115 |
116 | override init(frame: CGRect) {
117 | super.init(frame: frame)
118 | self.contentView.layer.borderColor = UIColor.gray.cgColor
119 | self.contentView.layer.borderWidth = 0.5
120 | self.contentView.addSubview(label)
121 | }
122 |
123 | required init?(coder: NSCoder) {
124 | fatalError("init(coder:) has not been implemented")
125 | }
126 |
127 | override func layoutSubviews() {
128 | super.layoutSubviews()
129 | self.label.frame = self.contentView.bounds
130 | }
131 |
132 | fileprivate func updateUI(state: UIView.ImpressionState?) {
133 | self.layer.removeAllAnimations()
134 | switch state {
135 | case .impressed(_, let areaRatio):
136 | self.label.text = String.init(format: "\(self.index)\n\n%0.1f%%", areaRatio * 100)
137 | self.contentView.backgroundColor = .green
138 | case .inScreen(_):
139 | self.contentView.backgroundColor = .white
140 | UIView.animate(withDuration: TimeInterval(self.contentView.durationThreshold ?? UIView.durationThreshold), delay: 0, options: [.curveLinear, .allowUserInteraction], animations: {
141 | self.contentView.backgroundColor = .red
142 | }, completion: nil)
143 | default:
144 | self.label.text = "\(self.index)\n\n"
145 | self.contentView.backgroundColor = .white
146 | }
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/ImpressionKit/ImpressionGroup.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImpressionGroup.swift
3 | // ImpressionKit
4 | //
5 | // Created by Yanni Wang on 2/6/21.
6 | //
7 |
8 | import UIKit
9 |
10 | public class ImpressionGroup {
11 |
12 | // MARK: - config
13 | // Change the detection (scan) interval (in seconds). Smaller detectionInterval means more accuracy and higher CPU consumption. Apply to the group. `UIView.detectionInterval` will be used if it's nil.
14 | public var detectionInterval: Float?
15 |
16 | // Chage the threshold of duration in screen (in seconds). The view will be impressed if it keeps being in screen after this seconds. Apply to the group. `UIView.durationThreshold` will be used if it's nil.
17 | public var durationThreshold: Float?
18 |
19 | // Chage the threshold of area ratio in screen. It's from 0 to 1. The view will be impressed if it's area ratio remains equal to or greater than this value. Apply to the group. `UIView.areaRatioThreshold` will be used if it's nil.
20 | public var areaRatioThreshold: Float?
21 |
22 | // Chage the threshold of alpha. It's from 0 to 1. The view will be impressed if it's alpha is equal to or greater than this value. Apply to the group. `UIView.alphaThreshold` will be used if it's nil.
23 | public var alphaThreshold: Float?
24 |
25 | // Retrigger the impression. Apply to the group. `UIView.redetectOptions` will be used if it's nil.
26 | public var redetectOptions: UIView.Redetect? {
27 | didSet {
28 | readdNotificationObserver()
29 | }
30 | }
31 |
32 | // MARK: - public
33 | public typealias ImpressionGroupCallback = (_ group: ImpressionGroup, _ index: IndexType, _ view: UIView, _ state: UIView.ImpressionState) -> ()
34 |
35 | public private(set) var states = [IndexType: UIView.ImpressionState]()
36 |
37 | // MARK: - private
38 | private var views = [IndexType: () -> UIView?]()
39 |
40 | private var notificationTokens = [NSObjectProtocol]()
41 |
42 | private let impressionGroupCallback: ImpressionGroupCallback
43 |
44 | private lazy var impressionBlock: (_ view: UIView, _ state: UIView.ImpressionState) -> () = { [weak self] (view, state) in
45 | guard let self = self,
46 | let index = self.views.first(where: { $1() == view })?.key else {
47 | return
48 | }
49 | if let previousSates = self.states[index] {
50 | guard previousSates != state else {
51 | return
52 | }
53 | }
54 |
55 | // Redetection of didEnterBackground & willResignActive are handled by group
56 |
57 | // redetect viewControllerDidDisappear
58 | if view.isRedetectionOn(.viewControllerDidDisappear),
59 | case .viewControllerDidDisappear = state {
60 | self.resetGroupStateAndRedetect(.viewControllerDidDisappear)
61 | return
62 | }
63 |
64 | // redetect leftScreen
65 | if let previousSates = self.states[index],
66 | previousSates.isImpressed {
67 | guard view.isRedetectionOn(.leftScreen) else {
68 | return
69 | }
70 | }
71 |
72 | self.changeState(index: index, view: view, state: state)
73 | }
74 |
75 | public init(detectionInterval: Float? = nil,
76 | durationThreshold: Float? = nil,
77 | areaRatioThreshold: Float? = nil,
78 | alphaThreshold: Float? = nil,
79 | redetectOptions: UIView.Redetect? = nil,
80 | impressionGroupCallback: @escaping ImpressionGroupCallback) {
81 | self.detectionInterval = detectionInterval
82 | self.durationThreshold = durationThreshold
83 | self.areaRatioThreshold = areaRatioThreshold
84 | self.alphaThreshold = alphaThreshold
85 | self.redetectOptions = redetectOptions
86 | self.impressionGroupCallback = impressionGroupCallback
87 | readdNotificationObserver()
88 | }
89 |
90 | /**
91 | This method must be called every time in
92 | 1. UICollectionView: `func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell`
93 | 2. or UITableView: `func cellForRow(at indexPath: IndexPath) -> UITableViewCell?`
94 | 3. or your customized methods.
95 | Non-calling may cause abnormal impression.
96 | if a index doesn't need to be impressed. pass `ignoreDetection = true` to ignore this index.
97 | */
98 | public func bind(view: UIView, index: IndexType, ignoreDetection: Bool = false) {
99 | if let previousIndex = views.first(where: { $1() == view })?.key {
100 | views[previousIndex] = nil
101 | }
102 |
103 | if let previousView = views[index]?() {
104 | previousView.detectImpression(nil)
105 | }
106 |
107 | guard ignoreDetection == false else {
108 | view.detectImpression(nil) // Cancel the detection if the view is detecting.
109 | self.changeState(index: index, view: view, state: .unknown)
110 | return
111 | }
112 |
113 | views[index] = { [weak view] in view }
114 |
115 | // setup views
116 | view.detectionInterval = self.detectionInterval
117 | view.durationThreshold = self.durationThreshold
118 | view.areaRatioThreshold = self.areaRatioThreshold
119 | view.alphaThreshold = self.alphaThreshold
120 | // No need to set .willResignActive and .didEnterBackground for views. Group will handle this.
121 | var redetectOptions = self.redetectOptions
122 | redetectOptions?.remove(.willResignActive)
123 | redetectOptions?.remove(.didEnterBackground)
124 | view.redetectOptions = redetectOptions
125 |
126 | guard let currentState = self.states[index],
127 | currentState.isImpressed else {
128 | self.changeState(index: index, view: view, state: .unknown)
129 | view.detectImpression(impressionBlock)
130 | return
131 | }
132 |
133 | // The view is impressed.
134 | guard view.keepDetectionAfterImpressed() else {
135 | view.detectImpression(nil)
136 | return
137 | }
138 |
139 | if view.isRedetectionOn(.leftScreen) {
140 | self.changeState(index: index, view: view, state: .unknown)
141 | }
142 | view.detectImpression(impressionBlock)
143 | }
144 |
145 | public func redetect() {
146 | resetGroupStateAndRedetect(.unknown)
147 | }
148 |
149 | private func resetGroupStateAndRedetect(_ state: UIView.ImpressionState) {
150 | self.views.forEach { (index, closure) in
151 | guard let view = closure() else {
152 | self.views.removeValue(forKey: index)
153 | return
154 | }
155 | self.changeState(index: index, view: view, state: state)
156 | view.detectImpression(impressionBlock)
157 | view.redetect()
158 | }
159 | self.states = self.states.mapValues { _ in state }
160 | }
161 |
162 | private func changeState(index: IndexType, view: UIView, state: UIView.ImpressionState) {
163 | if let previousState = self.states[index] {
164 | guard previousState != state else {
165 | return
166 | }
167 | }
168 | self.states[index] = state
169 | self.impressionGroupCallback(self, index, view, state)
170 | }
171 |
172 | private func readdNotificationObserver() {
173 | self.notificationTokens.forEach { (token) in
174 | NotificationCenter.default.removeObserver(token)
175 | }
176 | self.notificationTokens.removeAll()
177 | let redetectOptions = self.redetectOptions ?? UIView.redetectOptions
178 | if redetectOptions.contains(.willResignActive) {
179 | let token = NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: nil) { [weak self] _ in
180 | guard let self = self else {
181 | return
182 | }
183 | self.resetGroupStateAndRedetect(.willResignActive)
184 | }
185 | self.notificationTokens.append(token)
186 | }
187 | if redetectOptions.contains(.didEnterBackground) {
188 | let token = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { [weak self] _ in
189 | guard let self = self else {
190 | return
191 | }
192 | self.resetGroupStateAndRedetect(.didEnterBackground)
193 | }
194 | self.notificationTokens.append(token)
195 | }
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/ImpressionKit/UIViewPropertyExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIViewPropertyExtension.swift
3 | // ImpressionKit
4 | //
5 | // Created by Yanni Wang on 30/5/21.
6 | //
7 |
8 | import UIKit
9 | #if SWIFT_PACKAGE
10 | import SwiftHook
11 | #else
12 | import EasySwiftHook
13 | #endif
14 |
15 | private var stateKey = 0
16 | private var detectionIntervalKey = 0
17 | private var durationThresholdKey = 0
18 | private var areaRatioThresholdKey = 0
19 | private var alphaThresholdKey = 0
20 | private var redetectOptionsKey = 0
21 | private var hookingDeallocTokenKey = 0
22 | private var hookingDidMoveToWindowTokenKey = 0
23 | private var hookingViewDidDisappearTokenKey = 0
24 | private var notificationTokensKey = 0
25 | private var timerKey = 0
26 |
27 | extension UIView {
28 |
29 | // MARK: - Main
30 |
31 | public enum ImpressionState: Equatable {
32 | case unknown
33 | case impressed(atDate: Date, areaRatio: Float)
34 | case inScreen(fromDate: Date)
35 | case outOfScreen
36 | case noWindow
37 | case viewControllerDidDisappear
38 | case didEnterBackground
39 | case willResignActive
40 |
41 | public var isImpressed: Bool {
42 | if case .impressed = self {
43 | return true
44 | } else {
45 | return false
46 | }
47 | }
48 | }
49 |
50 | // Is triggered the impression event.
51 | public internal(set) var impressionState: ImpressionState {
52 | set {
53 | let old = self.impressionState
54 | objc_setAssociatedObject(self, &stateKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
55 | if old != newValue {
56 | guard let getCallback = self.getCallback() else {
57 | assert(false)
58 | return
59 | }
60 | getCallback(self, newValue)
61 | }
62 | }
63 |
64 | get {
65 | return (objc_getAssociatedObject(self, &stateKey) as? ImpressionState ?? .unknown)
66 | }
67 | }
68 |
69 | // MARK: - Config
70 |
71 | // Change the detection (scan) interval (in seconds). Smaller detectionInterval means more accuracy and higher CPU consumption. Apply to all views
72 | public static var detectionInterval: Float = 0.2
73 |
74 | // Change the detection (scan) interval (in seconds). Smaller detectionInterval means more accuracy and higher CPU consumption. Apply to the specific view. `UIView.detectionInterval` will be used if it's nil.
75 | public var detectionInterval: Float? {
76 | set {
77 | objc_setAssociatedObject(self, &detectionIntervalKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
78 | }
79 |
80 | get {
81 | return objc_getAssociatedObject(self, &detectionIntervalKey) as? Float
82 | }
83 | }
84 |
85 | // Chage the threshold of duration in screen (in seconds). The view will be impressed if it keeps being in screen after this seconds. Apply to all views
86 | public static var durationThreshold: Float = 1
87 |
88 | // Chage the threshold of duration in screen (in seconds). The view will be impressed if it keeps being in screen after this seconds. Apply to the specific view. `UIView.durationThreshold` will be used if it's nil.
89 | public var durationThreshold: Float? {
90 | set {
91 | objc_setAssociatedObject(self, &durationThresholdKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
92 | }
93 |
94 | get {
95 | return objc_getAssociatedObject(self, &durationThresholdKey) as? Float
96 | }
97 | }
98 |
99 | // Chage the threshold of area ratio in screen. It's from 0 to 1. The view will be impressed if it's area ratio remains equal to or greater than this value. Apply to all views
100 | public static var areaRatioThreshold: Float = 0.5
101 |
102 | // Chage the threshold of area ratio in screen. It's from 0 to 1. The view will be impressed if it's area ratio remains equal to or greater than this value. Apply to the specific view. `UIView.areaRatioThreshold` will be used if it's nil.
103 | public var areaRatioThreshold: Float? {
104 | set {
105 | objc_setAssociatedObject(self, &areaRatioThresholdKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
106 | }
107 |
108 | get {
109 | return objc_getAssociatedObject(self, &areaRatioThresholdKey) as? Float
110 | }
111 | }
112 |
113 | // Chage the threshold of alpha. It's from 0 to 1. The view will be impressed if it's alpha is equal to or greater than this value. Apply to all views
114 | public static var alphaThreshold: Float = 0.1
115 |
116 | // Chage the threshold of alpha. It's from 0 to 1. The view will be impressed if it's alpha is equal to or greater than this value. Apply to the specific view. `UIView.alphaThreshold` will be used if it's nil.
117 | public var alphaThreshold: Float? {
118 | set {
119 | objc_setAssociatedObject(self, &alphaThresholdKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
120 | }
121 |
122 | get {
123 | return objc_getAssociatedObject(self, &alphaThresholdKey) as? Float
124 | }
125 | }
126 |
127 | public struct Redetect: OptionSet {
128 |
129 | public let rawValue: Int
130 | public init(rawValue: Int) {
131 | self.rawValue = rawValue
132 | }
133 |
134 | // When a view left from the screen, mark the view to retrigger the impression event when this view come back to the screen.
135 | // Left screen means the view is out of the screen but the UIViewController is still there.
136 | public static let leftScreen = Redetect(rawValue: 1 << 0)
137 |
138 | // When the UIViewController of the view disappear, mark the view to retrigger the impression event when the UIViewController appear again.
139 | public static let viewControllerDidDisappear = Redetect(rawValue: 1 << 1)
140 |
141 | // When the app enter background, mark the view to retrigger the impression event when the app enter foreground.
142 | public static let didEnterBackground = Redetect(rawValue: 1 << 2)
143 |
144 | // When the app will resign active, mark the view to retrigger the impression event when the app become active.
145 | public static let willResignActive = Redetect(rawValue: 1 << 3)
146 | }
147 |
148 | // Retrigger the impression. Apply to all views.
149 | public static var redetectOptions: Redetect = []
150 |
151 | // Retrigger the impression. Apply to the specific view. `UIView.redetectOptions` will be used if it's nil.
152 | public var redetectOptions: Redetect? {
153 | set {
154 | objc_setAssociatedObject(self, &redetectOptionsKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
155 | }
156 |
157 | get {
158 | return objc_getAssociatedObject(self, &redetectOptionsKey) as? Redetect
159 | }
160 | }
161 |
162 | // MARK: - internal
163 |
164 | var hookingDeallocToken: Token? {
165 | set {
166 | let closure = { return newValue }
167 | objc_setAssociatedObject(self, &hookingDeallocTokenKey, closure, .OBJC_ASSOCIATION_COPY_NONATOMIC)
168 | }
169 |
170 | get {
171 | guard let closure = objc_getAssociatedObject(self, &hookingDeallocTokenKey) as? () -> Token? else { return nil }
172 | return closure()
173 | }
174 | }
175 |
176 | var hookingDidMoveToWindowToken: Token? {
177 | set {
178 | let closure = { return newValue }
179 | objc_setAssociatedObject(self, &hookingDidMoveToWindowTokenKey, closure, .OBJC_ASSOCIATION_COPY_NONATOMIC)
180 | }
181 |
182 | get {
183 | guard let closure = objc_getAssociatedObject(self, &hookingDidMoveToWindowTokenKey) as? () -> Token? else { return nil }
184 | return closure()
185 | }
186 | }
187 |
188 | var hookingViewDidDisappearToken: Token? {
189 | set {
190 | let closure = { return newValue }
191 | objc_setAssociatedObject(self, &hookingViewDidDisappearTokenKey, closure, .OBJC_ASSOCIATION_COPY_NONATOMIC)
192 | }
193 |
194 | get {
195 | guard let closure = objc_getAssociatedObject(self, &hookingViewDidDisappearTokenKey) as? () -> Token? else { return nil }
196 | return closure()
197 | }
198 | }
199 |
200 | var notificationTokens: [NSObjectProtocol] {
201 | set {
202 | objc_setAssociatedObject(self, ¬ificationTokensKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
203 | }
204 |
205 | get {
206 | return objc_getAssociatedObject(self, ¬ificationTokensKey) as? [NSObjectProtocol] ?? []
207 | }
208 | }
209 |
210 | var timer: Timer? {
211 | set {
212 | objc_setAssociatedObject(self, &timerKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
213 | }
214 | get {
215 | return objc_getAssociatedObject(self, &timerKey) as? Timer
216 | }
217 | }
218 |
219 | }
220 |
--------------------------------------------------------------------------------
/ImpressionKitExample/ImpressionKitExample/HomeViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeViewController.swift
3 | // ImpressionKitExample
4 | //
5 | // Created by Yanni Wang on 31/5/21.
6 | //
7 |
8 | import Foundation
9 | import Eureka
10 | import SwiftUI
11 |
12 | class HomeViewController: FormViewController {
13 |
14 | private static let detectionIntervalKey = "detectionIntervalKey"
15 | private static let durationThresholdKey = "durationThresholdKey"
16 | private static let areaRatioThresholdKey = "areaRatioThresholdKey"
17 | private static let alphaThresholdKey = "alphaThresholdKey"
18 | private static let alphaInDemoKey = "alphaInDemoKey"
19 | private static let redetectOptionsKey = "redetectOptionsKey"
20 |
21 | static var detectionInterval: Float {
22 | get {
23 | guard UserDefaults.standard.object(forKey: detectionIntervalKey) != nil else {
24 | return UIView.detectionInterval
25 | }
26 | return UserDefaults.standard.float(forKey: detectionIntervalKey)
27 | }
28 | set {
29 | UIView.detectionInterval = newValue
30 | UserDefaults.standard.set(newValue, forKey: detectionIntervalKey)
31 | }
32 | }
33 |
34 | static var durationThreshold: Float {
35 | get {
36 | guard UserDefaults.standard.object(forKey: durationThresholdKey) != nil else {
37 | return UIView.durationThreshold
38 | }
39 | return UserDefaults.standard.float(forKey: durationThresholdKey)
40 | }
41 | set {
42 | UIView.durationThreshold = newValue
43 | UserDefaults.standard.set(newValue, forKey: durationThresholdKey)
44 | }
45 | }
46 | static var areaRatioThreshold: Float {
47 | get {
48 | guard UserDefaults.standard.object(forKey: areaRatioThresholdKey) != nil else {
49 | return UIView.areaRatioThreshold
50 | }
51 | return UserDefaults.standard.float(forKey: areaRatioThresholdKey)
52 | }
53 | set {
54 | UIView.areaRatioThreshold = newValue
55 | UserDefaults.standard.set(newValue, forKey: areaRatioThresholdKey)
56 | }
57 | }
58 | static var alphaThreshold: Float {
59 | get {
60 | guard UserDefaults.standard.object(forKey: alphaThresholdKey) != nil else {
61 | return UIView.alphaThreshold
62 | }
63 | return UserDefaults.standard.float(forKey: alphaThresholdKey)
64 | }
65 | set {
66 | UIView.alphaThreshold = newValue
67 | UserDefaults.standard.set(newValue, forKey: alphaThresholdKey)
68 | }
69 | }
70 | static var alphaInDemo: Float {
71 | get {
72 | guard UserDefaults.standard.object(forKey: alphaInDemoKey) != nil else {
73 | return 1
74 | }
75 | return UserDefaults.standard.float(forKey: alphaInDemoKey)
76 | }
77 | set {
78 | UserDefaults.standard.set(newValue, forKey: alphaInDemoKey)
79 | }
80 | }
81 | static var redetectOptions: UIView.Redetect {
82 | get {
83 | guard UserDefaults.standard.object(forKey: redetectOptionsKey) != nil else {
84 | return UIView.redetectOptions
85 | }
86 | let value = UserDefaults.standard.integer(forKey: redetectOptionsKey)
87 | return UIView.Redetect.init(rawValue: value)
88 | }
89 | set {
90 | UIView.redetectOptions = newValue
91 | UserDefaults.standard.set(newValue.rawValue, forKey: redetectOptionsKey)
92 | }
93 | }
94 |
95 | override func viewDidLoad() {
96 | super.viewDidLoad()
97 | self.title = "ImpressionKit Demo";
98 | self.setUpForm()
99 | }
100 |
101 | func setUpForm() {
102 | CATransaction.begin()
103 | CATransaction.setDisableActions(true)
104 | form.removeAll()
105 | form +++ Section("Demos")
106 | <<< ButtonRow("UIScrollView") { row in
107 | row.title = row.tag
108 | row.presentationMode = .show(controllerProvider: ControllerProvider.callback(builder: { () -> UIViewController in
109 | return ScrollViewDemoViewController()
110 | }), onDismiss: nil)
111 | }
112 | <<< ButtonRow("UICollectionView (reusable views)") { row in
113 | row.title = row.tag
114 | row.presentationMode = .show(controllerProvider: ControllerProvider.callback(builder: { () -> UIViewController in
115 | return CollectionViewDemoViewController()
116 | }), onDismiss: nil)
117 | }
118 | <<< ButtonRow("UICollectionView (only detect 2nd section)") { row in
119 | row.title = row.tag
120 | row.presentationMode = .show(controllerProvider: ControllerProvider.callback(builder: { () -> UIViewController in
121 | return CollectionViewDemo2ViewController()
122 | }), onDismiss: nil)
123 | }
124 | <<< ButtonRow("UITableView (reusable views)") { row in
125 | row.title = row.tag
126 | row.presentationMode = .show(controllerProvider: ControllerProvider.callback(builder: { () -> UIViewController in
127 | return TableViewDemoViewController()
128 | }), onDismiss: nil)
129 | }
130 | <<< ButtonRow("SwiftUI ScrollView") { row in
131 | row.title = row.tag
132 | row.presentationMode = .show(controllerProvider: ControllerProvider.callback(builder: { () -> UIViewController in
133 | if #available(iOS 13.0, *) {
134 | return UIHostingController(rootView: SwiftUIScrollViewDemoView())
135 | } else {
136 | fatalError()
137 | }
138 | }), onDismiss: nil)
139 | }
140 | <<< ButtonRow("SwiftUI List (reusable views)") { row in
141 | row.title = row.tag
142 | row.presentationMode = .show(controllerProvider: ControllerProvider.callback(builder: { () -> UIViewController in
143 | if #available(iOS 13.0, *) {
144 | return UIHostingController(rootView: SwiftUIListDemoView())
145 | } else {
146 | fatalError()
147 | }
148 | }), onDismiss: nil)
149 | }
150 | +++ Section("SETTINGS")
151 | <<< DecimalRow() {
152 | $0.title = "Detection Interval"
153 | $0.value = Double(HomeViewController.detectionInterval)
154 | $0.formatter = DecimalFormatter()
155 | $0.useFormatterDuringInput = true
156 | //$0.useFormatterOnDidBeginEditing = true
157 | }.cellSetup { cell, _ in
158 | cell.textField.keyboardType = .numberPad
159 | }.onChange({ (row) in
160 | HomeViewController.detectionInterval = Float(row.value ?? 0)
161 | })
162 | <<< DecimalRow() {
163 | $0.title = "Duration Threshold"
164 | $0.value = Double(HomeViewController.durationThreshold)
165 | $0.formatter = DecimalFormatter()
166 | $0.useFormatterDuringInput = true
167 | //$0.useFormatterOnDidBeginEditing = true
168 | }.cellSetup { cell, _ in
169 | cell.textField.keyboardType = .numberPad
170 | }.onChange({ (row) in
171 | HomeViewController.durationThreshold = Float(row.value ?? 0)
172 | })
173 | <<< SliderRow() {
174 | $0.title = "Area Ratio Threshold"
175 | $0.value = Float(Int(HomeViewController.areaRatioThreshold * 100))
176 | $0.cell.slider.minimumValue = 1
177 | $0.cell.slider.maximumValue = 100
178 | $0.displayValueFor = {
179 | return "\(Int($0 ?? 0))%"
180 | }
181 | }.onChange({ (row) in
182 | HomeViewController.areaRatioThreshold = Float((row.value ?? 0) / 100)
183 | })
184 | <<< SliderRow() {
185 | $0.title = "Alpha Threshold"
186 | $0.value = Float(Int(HomeViewController.alphaThreshold * 100))
187 | $0.cell.slider.minimumValue = 1
188 | $0.cell.slider.maximumValue = 100
189 | $0.displayValueFor = {
190 | return "\(Int($0 ?? 0))%"
191 | }
192 | }.onChange({ (row) in
193 | HomeViewController.alphaThreshold = Float((row.value ?? 0) / 100)
194 | })
195 | <<< SliderRow() {
196 | $0.title = "Alpha of views in Demo"
197 | $0.value = Float(Int(HomeViewController.alphaInDemo * 100))
198 | $0.cell.slider.minimumValue = 1
199 | $0.cell.slider.maximumValue = 100
200 | $0.displayValueFor = {
201 | return "\(Int($0 ?? 0))%"
202 | }
203 | }.onChange({ (row) in
204 | HomeViewController.alphaInDemo = Float((row.value ?? 0) / 100)
205 | })
206 | <<< SwitchRow() {
207 | $0.title = "Redetect When Left Screen"
208 | $0.value = HomeViewController.redetectOptions.contains(.leftScreen)
209 | }.onChange({ (row) in
210 | if row.value ?? false {
211 | HomeViewController.redetectOptions.insert(.leftScreen)
212 | } else {
213 | HomeViewController.redetectOptions.remove(.leftScreen)
214 | }
215 | })
216 | <<< SwitchRow() {
217 | $0.title = "Redetect When DidDisappear"
218 | $0.value = HomeViewController.redetectOptions.contains(.viewControllerDidDisappear)
219 | }.onChange({ (row) in
220 | if row.value ?? false {
221 | HomeViewController.redetectOptions.insert(.viewControllerDidDisappear)
222 | } else {
223 | HomeViewController.redetectOptions.remove(.viewControllerDidDisappear)
224 | }
225 | })
226 | <<< SwitchRow() {
227 | $0.title = "Redetect When didEnterBackground"
228 | $0.value = HomeViewController.redetectOptions.contains(.didEnterBackground)
229 | }.onChange({ (row) in
230 | if row.value ?? false {
231 | HomeViewController.redetectOptions.insert(.didEnterBackground)
232 | } else {
233 | HomeViewController.redetectOptions.remove(.didEnterBackground)
234 | }
235 | })
236 | <<< SwitchRow() {
237 | $0.title = "Redetect When willResignActive"
238 | $0.value = HomeViewController.redetectOptions.contains(.willResignActive)
239 | }.onChange({ (row) in
240 | if row.value ?? false {
241 | HomeViewController.redetectOptions.insert(.willResignActive)
242 | } else {
243 | HomeViewController.redetectOptions.remove(.willResignActive)
244 | }
245 | })
246 | <<< ButtonRow(){
247 | $0.title = "Reset"
248 | $0.cell.tintColor = .red
249 | }.onCellSelection { [weak self] _,_ in
250 | HomeViewController.detectionInterval = 0.2
251 | HomeViewController.durationThreshold = 1
252 | HomeViewController.areaRatioThreshold = 0.5
253 | HomeViewController.alphaThreshold = 0.1
254 | HomeViewController.alphaInDemo = 1
255 | HomeViewController.redetectOptions = []
256 | self?.setUpForm()
257 | }
258 | CATransaction.commit()
259 | }
260 | }
261 |
262 |
--------------------------------------------------------------------------------
/ImpressionKit/UIViewFunctionExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImpressionKit.swift
3 | // ImpressionKit
4 | //
5 | // Created by Yanni Wang on 30/5/21.
6 | //
7 |
8 | import UIKit
9 | #if SWIFT_PACKAGE
10 | import SwiftHook
11 | #else
12 | import EasySwiftHook
13 | #endif
14 |
15 | private var impressionCallbackKey = 0
16 |
17 | public protocol ImpressionProtocol: UIView {}
18 |
19 | extension UIView: ImpressionProtocol {}
20 |
21 | public extension ImpressionProtocol {
22 |
23 | typealias ImpressionCallback = (_ view: ViewType, _ state: ImpressionState) -> ()
24 |
25 | // The callback will be triggered when impression happens. nil value means cancellation of detection
26 | func detectImpression(_ block: ImpressionCallback?) {
27 | assert(Thread.isMainThread);
28 | self.impressionState = .unknown
29 | if let block = block {
30 | // set
31 | let value = { view, state in
32 | guard let view = view as? Self else {
33 | return
34 | }
35 | block(view, state)
36 | } as ImpressionCallback
37 | objc_setAssociatedObject(self, &impressionCallbackKey, value, .OBJC_ASSOCIATION_COPY_NONATOMIC)
38 | // hook & notification
39 | self.hookDeallocIfNeeded()
40 | self.hookDidMoveToWindowIfNeeded()
41 | self.rehookViewDidDisappearIfNeeded()
42 | self.readdNotificationObserver()
43 | // timer
44 | self.startTimerIfNeeded()
45 | } else {
46 | // timer
47 | self.stopTimer()
48 | // cancel hook & notification
49 | self.removeNotificationObserverIfNeeded()
50 | self.cancelHookingViewDidDisappearIfNeeded()
51 | self.cancelHookingDidMoveToWindowIfNeeded()
52 | self.cancelHookingDeallocIfNeeded()
53 | // set
54 | objc_setAssociatedObject(self, &impressionCallbackKey, nil, .OBJC_ASSOCIATION_COPY_NONATOMIC)
55 | }
56 | }
57 |
58 | var isDetectionOn: Bool {
59 | return self.getCallback() != nil
60 | }
61 |
62 | internal func getCallback() -> ImpressionCallback? {
63 | return objc_getAssociatedObject(self, &impressionCallbackKey) as? ImpressionCallback
64 | }
65 | }
66 |
67 | extension UIView {
68 |
69 | // redetect impression for the UIView.
70 | public func redetect() {
71 | self.impressionState = .unknown
72 | self.startTimerIfNeeded()
73 | }
74 |
75 | func isRedetectionOn(_ option: Redetect) -> Bool {
76 | let redetectOptions = self.redetectOptions ?? UIView.redetectOptions
77 | return redetectOptions.contains(option)
78 | }
79 |
80 | func keepDetectionAfterImpressed() -> Bool {
81 | return self.isRedetectionOn(.leftScreen) || self.isRedetectionOn(.viewControllerDidDisappear)
82 | }
83 |
84 | // MARK: - Hook Dealloc
85 |
86 | fileprivate func hookDeallocIfNeeded() {
87 | guard self.hookingDeallocToken == nil else {
88 | return
89 | }
90 | self.hookingDeallocToken = try? hookDeallocBefore(object: self) { obj in
91 | obj.removeNotificationObserverIfNeeded()
92 | }
93 | }
94 |
95 | fileprivate func cancelHookingDeallocIfNeeded() {
96 | guard let token = self.hookingDeallocToken else {
97 | return
98 | }
99 | token.cancelHook()
100 | self.hookingDeallocToken = nil
101 | }
102 |
103 | // MARK: - Hook DidMoveToWindow
104 |
105 | fileprivate func hookDidMoveToWindowIfNeeded() {
106 | guard self.hookingDidMoveToWindowToken == nil else {
107 | return
108 | }
109 | self.hookingDidMoveToWindowToken = try? hookAfter(object: self, selector: #selector(UIView.didMoveToWindow), closure: { view, _ in
110 | if view.window != nil {
111 | if !view.impressionState.isImpressed {
112 | view.impressionState = .unknown
113 | }
114 | view.rehookViewDidDisappearIfNeeded()
115 | view.startTimerIfNeeded()
116 | } else {
117 | if !view.impressionState.isImpressed {
118 | view.impressionState = .noWindow
119 | }
120 | view.stopTimer()
121 | }
122 | } as @convention(block) (UIView, Selector) -> Void)
123 | }
124 |
125 | fileprivate func cancelHookingDidMoveToWindowIfNeeded() {
126 | guard let token = self.hookingDidMoveToWindowToken else {
127 | return
128 | }
129 | token.cancelHook()
130 | self.hookingDidMoveToWindowToken = nil
131 | }
132 |
133 | // MARK: - Hook ViewDidDisappear
134 |
135 | fileprivate func rehookViewDidDisappearIfNeeded() {
136 | self.cancelHookingViewDidDisappearIfNeeded()
137 | guard self.isRedetectionOn(.viewControllerDidDisappear) else {
138 | return
139 | }
140 |
141 | guard let vc = self.parentViewController else {
142 | return
143 | }
144 |
145 | var hookingViewDidDisappearToken: Token?
146 | hookingViewDidDisappearToken = try? hookAfter(object: vc, selector: #selector(UIViewController.viewDidDisappear(_:))) { [weak self] in
147 | guard let self = self,
148 | self.isRedetectionOn(.viewControllerDidDisappear) else {
149 | hookingViewDidDisappearToken?.cancelHook()
150 | return
151 | }
152 | self.impressionState = .viewControllerDidDisappear
153 | }
154 | self.hookingViewDidDisappearToken = hookingViewDidDisappearToken
155 | }
156 |
157 | fileprivate func cancelHookingViewDidDisappearIfNeeded() {
158 | guard let token = self.hookingViewDidDisappearToken else {
159 | return
160 | }
161 | token.cancelHook()
162 | self.hookingViewDidDisappearToken = nil
163 | }
164 |
165 | // MARK: - observe notifications
166 |
167 | fileprivate func readdNotificationObserver() {
168 | self.removeNotificationObserverIfNeeded()
169 | var names = [Notification.Name]()
170 | if self.isRedetectionOn(.didEnterBackground) {
171 | names.append(UIApplication.didEnterBackgroundNotification)
172 | }
173 | if self.isRedetectionOn(.willResignActive) {
174 | names.append(UIApplication.willResignActiveNotification)
175 | }
176 | guard names.count > 0 else {
177 | return
178 | }
179 | var tokens = [NSObjectProtocol]()
180 | for name in names {
181 | let token = NotificationCenter.default.addObserver(forName: name, object: nil, queue: nil, using: {[weak self] notification in
182 | guard let self = self else {
183 | return
184 | }
185 | if notification.name == UIApplication.didEnterBackgroundNotification {
186 | self.impressionState = .didEnterBackground
187 | } else if notification.name == UIApplication.willResignActiveNotification {
188 | self.impressionState = .willResignActive
189 | } else {
190 | assert(false)
191 | return
192 | }
193 | self.startTimerIfNeeded()
194 | })
195 | tokens.append(token)
196 | }
197 | self.notificationTokens = tokens
198 | }
199 |
200 | fileprivate func removeNotificationObserverIfNeeded() {
201 | self.notificationTokens.forEach { (token) in
202 | NotificationCenter.default.removeObserver(token)
203 | }
204 | self.notificationTokens.removeAll()
205 | }
206 |
207 | // MARK: - Algorithm
208 |
209 | private func areaRatio() -> Float {
210 | let alphaThreshold = CGFloat(self.alphaThreshold ?? UIView.alphaThreshold)
211 | guard self.isHidden == false && self.alpha >= alphaThreshold else {
212 | return 0
213 | }
214 | if let window = self as? UIWindow,
215 | self.superview == nil {
216 | // It's a root window
217 | let intersection = self.frame.intersection( window.screen.bounds)
218 | let ratio = (intersection.width * intersection.height) / (self.frame.width * self.frame.height)
219 | return self.fixRatioPrecision(number: Float(ratio))
220 | } else {
221 | // It's normal view
222 | guard let window = self.window,
223 | window.isHidden == false && window.alpha >= alphaThreshold else {
224 | return 0
225 | }
226 | // If super view hidden or alpha < alphaThreshold, self can't show
227 | var aView = self
228 | var frameInSuperView = self.bounds
229 | while let superView = aView.superview {
230 | guard superView.isHidden == false && superView.alpha >= alphaThreshold else {
231 | return 0
232 | }
233 | frameInSuperView = aView.convert(frameInSuperView, to: superView)
234 | if aView.clipsToBounds {
235 | frameInSuperView = frameInSuperView.intersection(aView.frame)
236 | }
237 | guard !frameInSuperView.isEmpty else {
238 | return 0
239 | }
240 | aView = superView
241 | }
242 | let frameInWindow = frameInSuperView
243 | let frameInScreen = CGRect.init(x: frameInWindow.origin.x + window.frame.origin.x,
244 | y: frameInWindow.origin.y + window.frame.origin.y,
245 | width: frameInWindow.width,
246 | height: frameInWindow.height)
247 | let intersection = frameInScreen.intersection(window.screen.bounds)
248 | let ratio = (intersection.width * intersection.height) / (self.frame.width * self.frame.height)
249 | return self.fixRatioPrecision(number: Float(ratio))
250 | }
251 | }
252 |
253 | private static let ratioPrecisionOffset: Float = 0.0001
254 | private func fixRatioPrecision(number: Float) -> Float {
255 | // As long as the different ratios on screen is within 0.01% (0.0001), then we can consider two ratios as equal. It's sufficient for this case.
256 | guard number > UIView.ratioPrecisionOffset else {
257 | return 0
258 | }
259 | guard number < 1 - UIView.ratioPrecisionOffset else {
260 | return 1
261 | }
262 | return number
263 | }
264 |
265 | private func detect() {
266 | if self.impressionState.isImpressed {
267 | guard self.keepDetectionAfterImpressed() else {
268 | // The impression has already been triggered. Don't need to retrigger.
269 | self.stopTimer()
270 | return
271 | }
272 | }
273 |
274 | // background
275 | if UIApplication.shared.applicationState == .background {
276 | guard !self.isRedetectionOn(.didEnterBackground) else {
277 | self.impressionState = .didEnterBackground
278 | return
279 | }
280 | }
281 |
282 | // inactive
283 | if UIApplication.shared.applicationState == .inactive {
284 | guard !self.isRedetectionOn(.willResignActive) else {
285 | self.impressionState = .willResignActive
286 | return
287 | }
288 | }
289 |
290 | // presented
291 | // If current view controller presented (non-full screen) a UIViewController, the viewDidDisappear of it will not be called. We need this logic to udpate the state.
292 | if let vc = self.parentViewController,
293 | vc.presentedViewController != nil {
294 | guard !self.isRedetectionOn(.viewControllerDidDisappear) else {
295 | self.impressionState = .viewControllerDidDisappear
296 | return
297 | }
298 | if !self.impressionState.isImpressed {
299 | self.impressionState = .viewControllerDidDisappear
300 | }
301 | return
302 | }
303 |
304 | // leftScreen
305 | if self.impressionState.isImpressed {
306 | guard self.isRedetectionOn(.leftScreen) else {
307 | return
308 | }
309 | }
310 |
311 | // calculate
312 | let areaRatio = self.areaRatio()
313 | let areaRatioThreshold = self.areaRatioThreshold ?? UIView.areaRatioThreshold
314 |
315 | if case .inScreen(let fromDate) = self.impressionState {
316 | if areaRatio >= areaRatioThreshold {
317 | // keep appearance
318 | let interval = Date().timeIntervalSince(fromDate)
319 | let durationThreshold = self.durationThreshold ?? UIView.durationThreshold
320 | if Float(interval) >= durationThreshold {
321 | // trigger impression
322 | self.impressionState = .impressed(atDate: Date(), areaRatio: areaRatio)
323 |
324 | if !self.keepDetectionAfterImpressed() {
325 | self.stopTimer()
326 | }
327 | }
328 | } else {
329 | // from appearance to disappearance
330 | self.impressionState = .outOfScreen
331 | }
332 | } else if areaRatio >= areaRatioThreshold {
333 | // appearance
334 | if !self.impressionState.isImpressed {
335 | self.impressionState = .inScreen(fromDate: Date())
336 | }
337 | } else {
338 | // disappearance
339 | self.impressionState = .outOfScreen
340 | }
341 | }
342 |
343 | // MARK: - timer
344 |
345 | fileprivate func startTimerIfNeeded() {
346 | if self.impressionState.isImpressed {
347 | guard self.keepDetectionAfterImpressed() else {
348 | return
349 | }
350 | }
351 | guard self.timer == nil,
352 | self.isDetectionOn,
353 | self.window != nil else {
354 | return
355 | }
356 | self.startTimer()
357 | }
358 |
359 | private func startTimer() {
360 | let timeInterval = TimeInterval(self.detectionInterval ?? UIView.detectionInterval)
361 | let timer = Timer.init(timeInterval: timeInterval, repeats: true, block: { [weak self] (timer) in
362 | // check self
363 | guard let self = self else {
364 | timer.invalidate()
365 | #if DEBUG
366 | ImpressionKitDebug.shared.timerCount -= 1
367 | #endif
368 | return
369 | }
370 | // check timer
371 | let currentTimeInterval = TimeInterval(self.detectionInterval ?? UIView.detectionInterval)
372 | guard currentTimeInterval.isEqual(to: timeInterval) else {
373 | self.stopTimer()
374 | self.startTimerIfNeeded()
375 | return
376 | }
377 | // detect
378 | self.detect()
379 | })
380 | self.timer = timer
381 | RunLoop.main.add(timer, forMode: .common)
382 | #if DEBUG
383 | ImpressionKitDebug.shared.timerCount += 1
384 | #endif
385 | }
386 |
387 | func stopTimer() {
388 | if let timer = self.timer {
389 | timer.invalidate()
390 | self.timer = nil
391 | #if DEBUG
392 | ImpressionKitDebug.shared.timerCount -= 1
393 | #endif
394 | }
395 | }
396 |
397 | // MARK: - others
398 | // https://stackoverflow.com/a/24590678/9315497
399 | private var parentViewController: UIViewController? {
400 | var parentResponder: UIResponder? = self
401 | while parentResponder != nil {
402 | parentResponder = parentResponder?.next
403 | if let viewController = parentResponder as? UIViewController {
404 | return viewController
405 | }
406 | }
407 | return nil
408 | }
409 |
410 | }
411 |
--------------------------------------------------------------------------------
/ImpressionKit.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 54;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 0F39B44B86894197DADB06D8 /* Pods_ImpressionKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 111BA5A553F376B7148F841D /* Pods_ImpressionKit.framework */; };
11 | 5C1ED03A26637C7900C68451 /* ImpressionKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C1ED03826637C7900C68451 /* ImpressionKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
12 | 5C1ED06B26637D5A00C68451 /* UIViewFunctionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1ED06A26637D5A00C68451 /* UIViewFunctionExtension.swift */; };
13 | 5C1ED0812663DD7B00C68451 /* UIViewPropertyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1ED0802663DD7B00C68451 /* UIViewPropertyExtension.swift */; };
14 | 5CDD55322667587B00FBF47E /* ImpressionGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDD55312667587B00FBF47E /* ImpressionGroup.swift */; };
15 | 62F7F59326A9708D003DF049 /* ImpressionKit+ViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62F7F59226A9708D003DF049 /* ImpressionKit+ViewModifier.swift */; };
16 | FC73BCDA27C99E92008B9765 /* ImpressionKitDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC73BCD927C99E92008B9765 /* ImpressionKitDebug.swift */; };
17 | /* End PBXBuildFile section */
18 |
19 | /* Begin PBXFileReference section */
20 | 111BA5A553F376B7148F841D /* Pods_ImpressionKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ImpressionKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
21 | 2C89A5CA1D58550E28745199 /* Pods-ImpressionKit.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ImpressionKit.release.xcconfig"; path = "Target Support Files/Pods-ImpressionKit/Pods-ImpressionKit.release.xcconfig"; sourceTree = ""; };
22 | 344B6F369D34D6690A7FED8E /* Pods-ImpressionKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ImpressionKit.debug.xcconfig"; path = "Target Support Files/Pods-ImpressionKit/Pods-ImpressionKit.debug.xcconfig"; sourceTree = ""; };
23 | 5C1ED03526637C7800C68451 /* ImpressionKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ImpressionKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
24 | 5C1ED03826637C7900C68451 /* ImpressionKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ImpressionKit.h; sourceTree = ""; };
25 | 5C1ED03926637C7900C68451 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
26 | 5C1ED06A26637D5A00C68451 /* UIViewFunctionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewFunctionExtension.swift; sourceTree = ""; };
27 | 5C1ED0802663DD7B00C68451 /* UIViewPropertyExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewPropertyExtension.swift; sourceTree = ""; };
28 | 5CDD55312667587B00FBF47E /* ImpressionGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImpressionGroup.swift; sourceTree = ""; };
29 | 62F7F59226A9708D003DF049 /* ImpressionKit+ViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ImpressionKit+ViewModifier.swift"; sourceTree = ""; };
30 | FC73BCD927C99E92008B9765 /* ImpressionKitDebug.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImpressionKitDebug.swift; sourceTree = ""; };
31 | /* End PBXFileReference section */
32 |
33 | /* Begin PBXFrameworksBuildPhase section */
34 | 5C1ED03226637C7800C68451 /* Frameworks */ = {
35 | isa = PBXFrameworksBuildPhase;
36 | buildActionMask = 2147483647;
37 | files = (
38 | 0F39B44B86894197DADB06D8 /* Pods_ImpressionKit.framework in Frameworks */,
39 | );
40 | runOnlyForDeploymentPostprocessing = 0;
41 | };
42 | /* End PBXFrameworksBuildPhase section */
43 |
44 | /* Begin PBXGroup section */
45 | 5C1ED02B26637C7800C68451 = {
46 | isa = PBXGroup;
47 | children = (
48 | 5C1ED03726637C7900C68451 /* ImpressionKit */,
49 | 5C1ED03626637C7800C68451 /* Products */,
50 | E4CF830392E091FC5199230D /* Pods */,
51 | F58AD94348A70BD8D3A51D7E /* Frameworks */,
52 | );
53 | sourceTree = "";
54 | };
55 | 5C1ED03626637C7800C68451 /* Products */ = {
56 | isa = PBXGroup;
57 | children = (
58 | 5C1ED03526637C7800C68451 /* ImpressionKit.framework */,
59 | );
60 | name = Products;
61 | sourceTree = "";
62 | };
63 | 5C1ED03726637C7900C68451 /* ImpressionKit */ = {
64 | isa = PBXGroup;
65 | children = (
66 | FC73BCD927C99E92008B9765 /* ImpressionKitDebug.swift */,
67 | 5C1ED03826637C7900C68451 /* ImpressionKit.h */,
68 | 5C1ED0802663DD7B00C68451 /* UIViewPropertyExtension.swift */,
69 | 5C1ED06A26637D5A00C68451 /* UIViewFunctionExtension.swift */,
70 | 5CDD55312667587B00FBF47E /* ImpressionGroup.swift */,
71 | 5C1ED03926637C7900C68451 /* Info.plist */,
72 | 62F7F59226A9708D003DF049 /* ImpressionKit+ViewModifier.swift */,
73 | );
74 | path = ImpressionKit;
75 | sourceTree = "";
76 | };
77 | E4CF830392E091FC5199230D /* Pods */ = {
78 | isa = PBXGroup;
79 | children = (
80 | 344B6F369D34D6690A7FED8E /* Pods-ImpressionKit.debug.xcconfig */,
81 | 2C89A5CA1D58550E28745199 /* Pods-ImpressionKit.release.xcconfig */,
82 | );
83 | path = Pods;
84 | sourceTree = "";
85 | };
86 | F58AD94348A70BD8D3A51D7E /* Frameworks */ = {
87 | isa = PBXGroup;
88 | children = (
89 | 111BA5A553F376B7148F841D /* Pods_ImpressionKit.framework */,
90 | );
91 | name = Frameworks;
92 | sourceTree = "";
93 | };
94 | /* End PBXGroup section */
95 |
96 | /* Begin PBXHeadersBuildPhase section */
97 | 5C1ED03026637C7800C68451 /* Headers */ = {
98 | isa = PBXHeadersBuildPhase;
99 | buildActionMask = 2147483647;
100 | files = (
101 | 5C1ED03A26637C7900C68451 /* ImpressionKit.h in Headers */,
102 | );
103 | runOnlyForDeploymentPostprocessing = 0;
104 | };
105 | /* End PBXHeadersBuildPhase section */
106 |
107 | /* Begin PBXNativeTarget section */
108 | 5C1ED03426637C7800C68451 /* ImpressionKit */ = {
109 | isa = PBXNativeTarget;
110 | buildConfigurationList = 5C1ED03D26637C7900C68451 /* Build configuration list for PBXNativeTarget "ImpressionKit" */;
111 | buildPhases = (
112 | 4324530236DA21B687DCA6F4 /* [CP] Check Pods Manifest.lock */,
113 | 5C1ED03026637C7800C68451 /* Headers */,
114 | 5C1ED03126637C7800C68451 /* Sources */,
115 | 5C1ED03226637C7800C68451 /* Frameworks */,
116 | 5C1ED03326637C7800C68451 /* Resources */,
117 | );
118 | buildRules = (
119 | );
120 | dependencies = (
121 | );
122 | name = ImpressionKit;
123 | productName = ImpressionKit;
124 | productReference = 5C1ED03526637C7800C68451 /* ImpressionKit.framework */;
125 | productType = "com.apple.product-type.framework";
126 | };
127 | /* End PBXNativeTarget section */
128 |
129 | /* Begin PBXProject section */
130 | 5C1ED02C26637C7800C68451 /* Project object */ = {
131 | isa = PBXProject;
132 | attributes = {
133 | BuildIndependentTargetsInParallel = YES;
134 | LastUpgradeCheck = 1630;
135 | TargetAttributes = {
136 | 5C1ED03426637C7800C68451 = {
137 | CreatedOnToolsVersion = 12.2;
138 | LastSwiftMigration = 1220;
139 | };
140 | };
141 | };
142 | buildConfigurationList = 5C1ED02F26637C7800C68451 /* Build configuration list for PBXProject "ImpressionKit" */;
143 | compatibilityVersion = "Xcode 9.3";
144 | developmentRegion = en;
145 | hasScannedForEncodings = 0;
146 | knownRegions = (
147 | en,
148 | Base,
149 | );
150 | mainGroup = 5C1ED02B26637C7800C68451;
151 | productRefGroup = 5C1ED03626637C7800C68451 /* Products */;
152 | projectDirPath = "";
153 | projectRoot = "";
154 | targets = (
155 | 5C1ED03426637C7800C68451 /* ImpressionKit */,
156 | );
157 | };
158 | /* End PBXProject section */
159 |
160 | /* Begin PBXResourcesBuildPhase section */
161 | 5C1ED03326637C7800C68451 /* Resources */ = {
162 | isa = PBXResourcesBuildPhase;
163 | buildActionMask = 2147483647;
164 | files = (
165 | );
166 | runOnlyForDeploymentPostprocessing = 0;
167 | };
168 | /* End PBXResourcesBuildPhase section */
169 |
170 | /* Begin PBXShellScriptBuildPhase section */
171 | 4324530236DA21B687DCA6F4 /* [CP] Check Pods Manifest.lock */ = {
172 | isa = PBXShellScriptBuildPhase;
173 | buildActionMask = 2147483647;
174 | files = (
175 | );
176 | inputFileListPaths = (
177 | );
178 | inputPaths = (
179 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
180 | "${PODS_ROOT}/Manifest.lock",
181 | );
182 | name = "[CP] Check Pods Manifest.lock";
183 | outputFileListPaths = (
184 | );
185 | outputPaths = (
186 | "$(DERIVED_FILE_DIR)/Pods-ImpressionKit-checkManifestLockResult.txt",
187 | );
188 | runOnlyForDeploymentPostprocessing = 0;
189 | shellPath = /bin/sh;
190 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
191 | showEnvVarsInLog = 0;
192 | };
193 | /* End PBXShellScriptBuildPhase section */
194 |
195 | /* Begin PBXSourcesBuildPhase section */
196 | 5C1ED03126637C7800C68451 /* Sources */ = {
197 | isa = PBXSourcesBuildPhase;
198 | buildActionMask = 2147483647;
199 | files = (
200 | 5C1ED06B26637D5A00C68451 /* UIViewFunctionExtension.swift in Sources */,
201 | FC73BCDA27C99E92008B9765 /* ImpressionKitDebug.swift in Sources */,
202 | 62F7F59326A9708D003DF049 /* ImpressionKit+ViewModifier.swift in Sources */,
203 | 5CDD55322667587B00FBF47E /* ImpressionGroup.swift in Sources */,
204 | 5C1ED0812663DD7B00C68451 /* UIViewPropertyExtension.swift in Sources */,
205 | );
206 | runOnlyForDeploymentPostprocessing = 0;
207 | };
208 | /* End PBXSourcesBuildPhase section */
209 |
210 | /* Begin XCBuildConfiguration section */
211 | 5C1ED03B26637C7900C68451 /* Debug */ = {
212 | isa = XCBuildConfiguration;
213 | buildSettings = {
214 | ALWAYS_SEARCH_USER_PATHS = NO;
215 | CLANG_ANALYZER_NONNULL = YES;
216 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
217 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
218 | CLANG_CXX_LIBRARY = "libc++";
219 | CLANG_ENABLE_MODULES = YES;
220 | CLANG_ENABLE_OBJC_ARC = YES;
221 | CLANG_ENABLE_OBJC_WEAK = YES;
222 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
223 | CLANG_WARN_BOOL_CONVERSION = YES;
224 | CLANG_WARN_COMMA = YES;
225 | CLANG_WARN_CONSTANT_CONVERSION = YES;
226 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
227 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
228 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
229 | CLANG_WARN_EMPTY_BODY = YES;
230 | CLANG_WARN_ENUM_CONVERSION = YES;
231 | CLANG_WARN_INFINITE_RECURSION = YES;
232 | CLANG_WARN_INT_CONVERSION = YES;
233 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
234 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
235 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
236 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
237 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
238 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
239 | CLANG_WARN_STRICT_PROTOTYPES = YES;
240 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
241 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
242 | CLANG_WARN_UNREACHABLE_CODE = YES;
243 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
244 | COPY_PHASE_STRIP = NO;
245 | CURRENT_PROJECT_VERSION = 1;
246 | DEBUG_INFORMATION_FORMAT = dwarf;
247 | DEVELOPMENT_TEAM = ZQ7TZ3Q272;
248 | ENABLE_STRICT_OBJC_MSGSEND = YES;
249 | ENABLE_TESTABILITY = YES;
250 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
251 | GCC_C_LANGUAGE_STANDARD = gnu11;
252 | GCC_DYNAMIC_NO_PIC = NO;
253 | GCC_NO_COMMON_BLOCKS = YES;
254 | GCC_OPTIMIZATION_LEVEL = 0;
255 | GCC_PREPROCESSOR_DEFINITIONS = (
256 | "DEBUG=1",
257 | "$(inherited)",
258 | );
259 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
260 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
261 | GCC_WARN_UNDECLARED_SELECTOR = YES;
262 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
263 | GCC_WARN_UNUSED_FUNCTION = YES;
264 | GCC_WARN_UNUSED_VARIABLE = YES;
265 | IPHONEOS_DEPLOYMENT_TARGET = 12.0;
266 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
267 | MTL_FAST_MATH = YES;
268 | ONLY_ACTIVE_ARCH = YES;
269 | SDKROOT = iphoneos;
270 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
271 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
272 | VERSIONING_SYSTEM = "apple-generic";
273 | VERSION_INFO_PREFIX = "";
274 | };
275 | name = Debug;
276 | };
277 | 5C1ED03C26637C7900C68451 /* Release */ = {
278 | isa = XCBuildConfiguration;
279 | buildSettings = {
280 | ALWAYS_SEARCH_USER_PATHS = NO;
281 | CLANG_ANALYZER_NONNULL = YES;
282 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
283 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
284 | CLANG_CXX_LIBRARY = "libc++";
285 | CLANG_ENABLE_MODULES = YES;
286 | CLANG_ENABLE_OBJC_ARC = YES;
287 | CLANG_ENABLE_OBJC_WEAK = YES;
288 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
289 | CLANG_WARN_BOOL_CONVERSION = YES;
290 | CLANG_WARN_COMMA = YES;
291 | CLANG_WARN_CONSTANT_CONVERSION = YES;
292 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
293 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
294 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
295 | CLANG_WARN_EMPTY_BODY = YES;
296 | CLANG_WARN_ENUM_CONVERSION = YES;
297 | CLANG_WARN_INFINITE_RECURSION = YES;
298 | CLANG_WARN_INT_CONVERSION = YES;
299 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
300 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
301 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
302 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
303 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
304 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
305 | CLANG_WARN_STRICT_PROTOTYPES = YES;
306 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
307 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
308 | CLANG_WARN_UNREACHABLE_CODE = YES;
309 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
310 | COPY_PHASE_STRIP = NO;
311 | CURRENT_PROJECT_VERSION = 1;
312 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
313 | DEVELOPMENT_TEAM = ZQ7TZ3Q272;
314 | ENABLE_NS_ASSERTIONS = NO;
315 | ENABLE_STRICT_OBJC_MSGSEND = YES;
316 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
317 | GCC_C_LANGUAGE_STANDARD = gnu11;
318 | GCC_NO_COMMON_BLOCKS = YES;
319 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
320 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
321 | GCC_WARN_UNDECLARED_SELECTOR = YES;
322 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
323 | GCC_WARN_UNUSED_FUNCTION = YES;
324 | GCC_WARN_UNUSED_VARIABLE = YES;
325 | IPHONEOS_DEPLOYMENT_TARGET = 12.0;
326 | MTL_ENABLE_DEBUG_INFO = NO;
327 | MTL_FAST_MATH = YES;
328 | SDKROOT = iphoneos;
329 | SWIFT_COMPILATION_MODE = wholemodule;
330 | SWIFT_OPTIMIZATION_LEVEL = "-O";
331 | VALIDATE_PRODUCT = YES;
332 | VERSIONING_SYSTEM = "apple-generic";
333 | VERSION_INFO_PREFIX = "";
334 | };
335 | name = Release;
336 | };
337 | 5C1ED03E26637C7900C68451 /* Debug */ = {
338 | isa = XCBuildConfiguration;
339 | baseConfigurationReference = 344B6F369D34D6690A7FED8E /* Pods-ImpressionKit.debug.xcconfig */;
340 | buildSettings = {
341 | CLANG_ENABLE_MODULES = YES;
342 | CODE_SIGN_IDENTITY = "";
343 | CODE_SIGN_STYLE = Automatic;
344 | DEFINES_MODULE = YES;
345 | DYLIB_COMPATIBILITY_VERSION = 1;
346 | DYLIB_CURRENT_VERSION = 1;
347 | DYLIB_INSTALL_NAME_BASE = "@rpath";
348 | ENABLE_MODULE_VERIFIER = YES;
349 | INFOPLIST_FILE = ImpressionKit/Info.plist;
350 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
351 | LD_RUNPATH_SEARCH_PATHS = (
352 | "$(inherited)",
353 | "@executable_path/Frameworks",
354 | "@loader_path/Frameworks",
355 | );
356 | MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
357 | PRODUCT_BUNDLE_IDENTIFIER = com.yanni.ImpressionKit;
358 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
359 | SKIP_INSTALL = YES;
360 | SUPPORTS_MACCATALYST = NO;
361 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
362 | SWIFT_VERSION = 5.0;
363 | TARGETED_DEVICE_FAMILY = "1,2";
364 | };
365 | name = Debug;
366 | };
367 | 5C1ED03F26637C7900C68451 /* Release */ = {
368 | isa = XCBuildConfiguration;
369 | baseConfigurationReference = 2C89A5CA1D58550E28745199 /* Pods-ImpressionKit.release.xcconfig */;
370 | buildSettings = {
371 | CLANG_ENABLE_MODULES = YES;
372 | CODE_SIGN_IDENTITY = "";
373 | CODE_SIGN_STYLE = Automatic;
374 | DEFINES_MODULE = YES;
375 | DYLIB_COMPATIBILITY_VERSION = 1;
376 | DYLIB_CURRENT_VERSION = 1;
377 | DYLIB_INSTALL_NAME_BASE = "@rpath";
378 | ENABLE_MODULE_VERIFIER = YES;
379 | INFOPLIST_FILE = ImpressionKit/Info.plist;
380 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
381 | LD_RUNPATH_SEARCH_PATHS = (
382 | "$(inherited)",
383 | "@executable_path/Frameworks",
384 | "@loader_path/Frameworks",
385 | );
386 | MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
387 | PRODUCT_BUNDLE_IDENTIFIER = com.yanni.ImpressionKit;
388 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
389 | SKIP_INSTALL = YES;
390 | SUPPORTS_MACCATALYST = NO;
391 | SWIFT_VERSION = 5.0;
392 | TARGETED_DEVICE_FAMILY = "1,2";
393 | };
394 | name = Release;
395 | };
396 | /* End XCBuildConfiguration section */
397 |
398 | /* Begin XCConfigurationList section */
399 | 5C1ED02F26637C7800C68451 /* Build configuration list for PBXProject "ImpressionKit" */ = {
400 | isa = XCConfigurationList;
401 | buildConfigurations = (
402 | 5C1ED03B26637C7900C68451 /* Debug */,
403 | 5C1ED03C26637C7900C68451 /* Release */,
404 | );
405 | defaultConfigurationIsVisible = 0;
406 | defaultConfigurationName = Release;
407 | };
408 | 5C1ED03D26637C7900C68451 /* Build configuration list for PBXNativeTarget "ImpressionKit" */ = {
409 | isa = XCConfigurationList;
410 | buildConfigurations = (
411 | 5C1ED03E26637C7900C68451 /* Debug */,
412 | 5C1ED03F26637C7900C68451 /* Release */,
413 | );
414 | defaultConfigurationIsVisible = 0;
415 | defaultConfigurationName = Release;
416 | };
417 | /* End XCConfigurationList section */
418 | };
419 | rootObject = 5C1ED02C26637C7800C68451 /* Project object */;
420 | }
421 |
--------------------------------------------------------------------------------
/ImpressionKitExample/ImpressionKitExample.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 54;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 2AA9DA45279A9E7200A9C06A /* CollectionViewDemo2ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AA9DA44279A9E7200A9C06A /* CollectionViewDemo2ViewController.swift */; };
11 | 5C1ED04E26637CB000C68451 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1ED04D26637CB000C68451 /* AppDelegate.swift */; };
12 | 5C1ED05726637CB000C68451 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5C1ED05626637CB000C68451 /* Assets.xcassets */; };
13 | 5C1ED05A26637CB000C68451 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5C1ED05826637CB000C68451 /* LaunchScreen.storyboard */; };
14 | 5C4B30D4266D026900B61135 /* TableViewDemoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B30D3266D026900B61135 /* TableViewDemoViewController.swift */; };
15 | 5C87C65C2664DB48006E824E /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C87C65B2664DB48006E824E /* HomeViewController.swift */; };
16 | 5C87C6612664F2B5006E824E /* ScrollViewDemoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C87C6602664F2B5006E824E /* ScrollViewDemoViewController.swift */; };
17 | 5C87C6662664F327006E824E /* CollectionViewDemoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C87C6652664F327006E824E /* CollectionViewDemoViewController.swift */; };
18 | 62F7F59526A973A9003DF049 /* SwiftUIListDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62F7F59426A973A9003DF049 /* SwiftUIListDemoView.swift */; };
19 | 634294AE6E2BB68D33D5D7F6 /* Pods_ImpressionKitExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E14EEC2186D571924183418 /* Pods_ImpressionKitExample.framework */; };
20 | 9CB13B7526AD634900A510F9 /* SwiftUIScrollViewDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CB13B7426AD634900A510F9 /* SwiftUIScrollViewDemoView.swift */; };
21 | /* End PBXBuildFile section */
22 |
23 | /* Begin PBXFileReference section */
24 | 2AA9DA44279A9E7200A9C06A /* CollectionViewDemo2ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewDemo2ViewController.swift; sourceTree = ""; };
25 | 5C1ED04A26637CB000C68451 /* ImpressionKitExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ImpressionKitExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
26 | 5C1ED04D26637CB000C68451 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
27 | 5C1ED05626637CB000C68451 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
28 | 5C1ED05926637CB000C68451 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
29 | 5C1ED05B26637CB000C68451 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
30 | 5C4B30D3266D026900B61135 /* TableViewDemoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableViewDemoViewController.swift; sourceTree = ""; };
31 | 5C87C65B2664DB48006E824E /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; };
32 | 5C87C6602664F2B5006E824E /* ScrollViewDemoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewDemoViewController.swift; sourceTree = ""; };
33 | 5C87C6652664F327006E824E /* CollectionViewDemoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewDemoViewController.swift; sourceTree = ""; };
34 | 62F7F59426A973A9003DF049 /* SwiftUIListDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIListDemoView.swift; sourceTree = ""; };
35 | 6E14EEC2186D571924183418 /* Pods_ImpressionKitExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ImpressionKitExample.framework; sourceTree = BUILT_PRODUCTS_DIR; };
36 | 9CB13B7426AD634900A510F9 /* SwiftUIScrollViewDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIScrollViewDemoView.swift; sourceTree = ""; };
37 | D6FDACE8F7FE7BF61899FD4A /* Pods-ImpressionKitExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ImpressionKitExample.debug.xcconfig"; path = "Target Support Files/Pods-ImpressionKitExample/Pods-ImpressionKitExample.debug.xcconfig"; sourceTree = ""; };
38 | E93E5965889DA252ED3ECF35 /* Pods-ImpressionKitExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ImpressionKitExample.release.xcconfig"; path = "Target Support Files/Pods-ImpressionKitExample/Pods-ImpressionKitExample.release.xcconfig"; sourceTree = ""; };
39 | /* End PBXFileReference section */
40 |
41 | /* Begin PBXFrameworksBuildPhase section */
42 | 5C1ED04726637CB000C68451 /* Frameworks */ = {
43 | isa = PBXFrameworksBuildPhase;
44 | buildActionMask = 2147483647;
45 | files = (
46 | 634294AE6E2BB68D33D5D7F6 /* Pods_ImpressionKitExample.framework in Frameworks */,
47 | );
48 | runOnlyForDeploymentPostprocessing = 0;
49 | };
50 | /* End PBXFrameworksBuildPhase section */
51 |
52 | /* Begin PBXGroup section */
53 | 0AADC43F91C62FB71E41F54E /* Frameworks */ = {
54 | isa = PBXGroup;
55 | children = (
56 | 6E14EEC2186D571924183418 /* Pods_ImpressionKitExample.framework */,
57 | );
58 | name = Frameworks;
59 | sourceTree = "";
60 | };
61 | 5C1ED04126637CB000C68451 = {
62 | isa = PBXGroup;
63 | children = (
64 | 5C1ED04C26637CB000C68451 /* ImpressionKitExample */,
65 | 5C1ED04B26637CB000C68451 /* Products */,
66 | 647CED7184DD762C73417487 /* Pods */,
67 | 0AADC43F91C62FB71E41F54E /* Frameworks */,
68 | );
69 | sourceTree = "";
70 | };
71 | 5C1ED04B26637CB000C68451 /* Products */ = {
72 | isa = PBXGroup;
73 | children = (
74 | 5C1ED04A26637CB000C68451 /* ImpressionKitExample.app */,
75 | );
76 | name = Products;
77 | sourceTree = "";
78 | };
79 | 5C1ED04C26637CB000C68451 /* ImpressionKitExample */ = {
80 | isa = PBXGroup;
81 | children = (
82 | 5C1ED04D26637CB000C68451 /* AppDelegate.swift */,
83 | 5C87C65B2664DB48006E824E /* HomeViewController.swift */,
84 | 5C87C6602664F2B5006E824E /* ScrollViewDemoViewController.swift */,
85 | 5C87C6652664F327006E824E /* CollectionViewDemoViewController.swift */,
86 | 2AA9DA44279A9E7200A9C06A /* CollectionViewDemo2ViewController.swift */,
87 | 5C4B30D3266D026900B61135 /* TableViewDemoViewController.swift */,
88 | 9CB13B7426AD634900A510F9 /* SwiftUIScrollViewDemoView.swift */,
89 | 62F7F59426A973A9003DF049 /* SwiftUIListDemoView.swift */,
90 | 5C1ED05626637CB000C68451 /* Assets.xcassets */,
91 | 5C1ED05826637CB000C68451 /* LaunchScreen.storyboard */,
92 | 5C1ED05B26637CB000C68451 /* Info.plist */,
93 | );
94 | path = ImpressionKitExample;
95 | sourceTree = "";
96 | };
97 | 647CED7184DD762C73417487 /* Pods */ = {
98 | isa = PBXGroup;
99 | children = (
100 | D6FDACE8F7FE7BF61899FD4A /* Pods-ImpressionKitExample.debug.xcconfig */,
101 | E93E5965889DA252ED3ECF35 /* Pods-ImpressionKitExample.release.xcconfig */,
102 | );
103 | path = Pods;
104 | sourceTree = "";
105 | };
106 | /* End PBXGroup section */
107 |
108 | /* Begin PBXNativeTarget section */
109 | 5C1ED04926637CB000C68451 /* ImpressionKitExample */ = {
110 | isa = PBXNativeTarget;
111 | buildConfigurationList = 5C1ED05E26637CB000C68451 /* Build configuration list for PBXNativeTarget "ImpressionKitExample" */;
112 | buildPhases = (
113 | D18904B0EE4D0A65025D86B4 /* [CP] Check Pods Manifest.lock */,
114 | 5C1ED04626637CB000C68451 /* Sources */,
115 | 5C1ED04726637CB000C68451 /* Frameworks */,
116 | 5C1ED04826637CB000C68451 /* Resources */,
117 | BADCF342DB72A74F92A19C9D /* [CP] Embed Pods Frameworks */,
118 | );
119 | buildRules = (
120 | );
121 | dependencies = (
122 | );
123 | name = ImpressionKitExample;
124 | productName = ImpressionKitExample;
125 | productReference = 5C1ED04A26637CB000C68451 /* ImpressionKitExample.app */;
126 | productType = "com.apple.product-type.application";
127 | };
128 | /* End PBXNativeTarget section */
129 |
130 | /* Begin PBXProject section */
131 | 5C1ED04226637CB000C68451 /* Project object */ = {
132 | isa = PBXProject;
133 | attributes = {
134 | LastSwiftUpdateCheck = 1220;
135 | LastUpgradeCheck = 1220;
136 | TargetAttributes = {
137 | 5C1ED04926637CB000C68451 = {
138 | CreatedOnToolsVersion = 12.2;
139 | };
140 | };
141 | };
142 | buildConfigurationList = 5C1ED04526637CB000C68451 /* Build configuration list for PBXProject "ImpressionKitExample" */;
143 | compatibilityVersion = "Xcode 9.3";
144 | developmentRegion = en;
145 | hasScannedForEncodings = 0;
146 | knownRegions = (
147 | en,
148 | Base,
149 | );
150 | mainGroup = 5C1ED04126637CB000C68451;
151 | productRefGroup = 5C1ED04B26637CB000C68451 /* Products */;
152 | projectDirPath = "";
153 | projectRoot = "";
154 | targets = (
155 | 5C1ED04926637CB000C68451 /* ImpressionKitExample */,
156 | );
157 | };
158 | /* End PBXProject section */
159 |
160 | /* Begin PBXResourcesBuildPhase section */
161 | 5C1ED04826637CB000C68451 /* Resources */ = {
162 | isa = PBXResourcesBuildPhase;
163 | buildActionMask = 2147483647;
164 | files = (
165 | 5C1ED05A26637CB000C68451 /* LaunchScreen.storyboard in Resources */,
166 | 5C1ED05726637CB000C68451 /* Assets.xcassets in Resources */,
167 | );
168 | runOnlyForDeploymentPostprocessing = 0;
169 | };
170 | /* End PBXResourcesBuildPhase section */
171 |
172 | /* Begin PBXShellScriptBuildPhase section */
173 | BADCF342DB72A74F92A19C9D /* [CP] Embed Pods Frameworks */ = {
174 | isa = PBXShellScriptBuildPhase;
175 | buildActionMask = 2147483647;
176 | files = (
177 | );
178 | inputFileListPaths = (
179 | "${PODS_ROOT}/Target Support Files/Pods-ImpressionKitExample/Pods-ImpressionKitExample-frameworks-${CONFIGURATION}-input-files.xcfilelist",
180 | );
181 | name = "[CP] Embed Pods Frameworks";
182 | outputFileListPaths = (
183 | "${PODS_ROOT}/Target Support Files/Pods-ImpressionKitExample/Pods-ImpressionKitExample-frameworks-${CONFIGURATION}-output-files.xcfilelist",
184 | );
185 | runOnlyForDeploymentPostprocessing = 0;
186 | shellPath = /bin/sh;
187 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ImpressionKitExample/Pods-ImpressionKitExample-frameworks.sh\"\n";
188 | showEnvVarsInLog = 0;
189 | };
190 | D18904B0EE4D0A65025D86B4 /* [CP] Check Pods Manifest.lock */ = {
191 | isa = PBXShellScriptBuildPhase;
192 | buildActionMask = 2147483647;
193 | files = (
194 | );
195 | inputFileListPaths = (
196 | );
197 | inputPaths = (
198 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
199 | "${PODS_ROOT}/Manifest.lock",
200 | );
201 | name = "[CP] Check Pods Manifest.lock";
202 | outputFileListPaths = (
203 | );
204 | outputPaths = (
205 | "$(DERIVED_FILE_DIR)/Pods-ImpressionKitExample-checkManifestLockResult.txt",
206 | );
207 | runOnlyForDeploymentPostprocessing = 0;
208 | shellPath = /bin/sh;
209 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
210 | showEnvVarsInLog = 0;
211 | };
212 | /* End PBXShellScriptBuildPhase section */
213 |
214 | /* Begin PBXSourcesBuildPhase section */
215 | 5C1ED04626637CB000C68451 /* Sources */ = {
216 | isa = PBXSourcesBuildPhase;
217 | buildActionMask = 2147483647;
218 | files = (
219 | 5C87C6662664F327006E824E /* CollectionViewDemoViewController.swift in Sources */,
220 | 62F7F59526A973A9003DF049 /* SwiftUIListDemoView.swift in Sources */,
221 | 5C87C6612664F2B5006E824E /* ScrollViewDemoViewController.swift in Sources */,
222 | 9CB13B7526AD634900A510F9 /* SwiftUIScrollViewDemoView.swift in Sources */,
223 | 5C87C65C2664DB48006E824E /* HomeViewController.swift in Sources */,
224 | 5C4B30D4266D026900B61135 /* TableViewDemoViewController.swift in Sources */,
225 | 5C1ED04E26637CB000C68451 /* AppDelegate.swift in Sources */,
226 | 2AA9DA45279A9E7200A9C06A /* CollectionViewDemo2ViewController.swift in Sources */,
227 | );
228 | runOnlyForDeploymentPostprocessing = 0;
229 | };
230 | /* End PBXSourcesBuildPhase section */
231 |
232 | /* Begin PBXVariantGroup section */
233 | 5C1ED05826637CB000C68451 /* LaunchScreen.storyboard */ = {
234 | isa = PBXVariantGroup;
235 | children = (
236 | 5C1ED05926637CB000C68451 /* Base */,
237 | );
238 | name = LaunchScreen.storyboard;
239 | sourceTree = "";
240 | };
241 | /* End PBXVariantGroup section */
242 |
243 | /* Begin XCBuildConfiguration section */
244 | 5C1ED05C26637CB000C68451 /* Debug */ = {
245 | isa = XCBuildConfiguration;
246 | buildSettings = {
247 | ALWAYS_SEARCH_USER_PATHS = NO;
248 | CLANG_ANALYZER_NONNULL = YES;
249 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
250 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
251 | CLANG_CXX_LIBRARY = "libc++";
252 | CLANG_ENABLE_MODULES = YES;
253 | CLANG_ENABLE_OBJC_ARC = YES;
254 | CLANG_ENABLE_OBJC_WEAK = YES;
255 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
256 | CLANG_WARN_BOOL_CONVERSION = YES;
257 | CLANG_WARN_COMMA = YES;
258 | CLANG_WARN_CONSTANT_CONVERSION = YES;
259 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
260 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
261 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
262 | CLANG_WARN_EMPTY_BODY = YES;
263 | CLANG_WARN_ENUM_CONVERSION = YES;
264 | CLANG_WARN_INFINITE_RECURSION = YES;
265 | CLANG_WARN_INT_CONVERSION = YES;
266 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
267 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
268 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
269 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
270 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
271 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
272 | CLANG_WARN_STRICT_PROTOTYPES = YES;
273 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
274 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
275 | CLANG_WARN_UNREACHABLE_CODE = YES;
276 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
277 | COPY_PHASE_STRIP = NO;
278 | DEBUG_INFORMATION_FORMAT = dwarf;
279 | ENABLE_STRICT_OBJC_MSGSEND = YES;
280 | ENABLE_TESTABILITY = YES;
281 | GCC_C_LANGUAGE_STANDARD = gnu11;
282 | GCC_DYNAMIC_NO_PIC = NO;
283 | GCC_NO_COMMON_BLOCKS = YES;
284 | GCC_OPTIMIZATION_LEVEL = 0;
285 | GCC_PREPROCESSOR_DEFINITIONS = (
286 | "DEBUG=1",
287 | "$(inherited)",
288 | );
289 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
290 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
291 | GCC_WARN_UNDECLARED_SELECTOR = YES;
292 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
293 | GCC_WARN_UNUSED_FUNCTION = YES;
294 | GCC_WARN_UNUSED_VARIABLE = YES;
295 | IPHONEOS_DEPLOYMENT_TARGET = 12.0;
296 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
297 | MTL_FAST_MATH = YES;
298 | ONLY_ACTIVE_ARCH = YES;
299 | SDKROOT = iphoneos;
300 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
301 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
302 | };
303 | name = Debug;
304 | };
305 | 5C1ED05D26637CB000C68451 /* Release */ = {
306 | isa = XCBuildConfiguration;
307 | buildSettings = {
308 | ALWAYS_SEARCH_USER_PATHS = NO;
309 | CLANG_ANALYZER_NONNULL = YES;
310 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
311 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
312 | CLANG_CXX_LIBRARY = "libc++";
313 | CLANG_ENABLE_MODULES = YES;
314 | CLANG_ENABLE_OBJC_ARC = YES;
315 | CLANG_ENABLE_OBJC_WEAK = YES;
316 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
317 | CLANG_WARN_BOOL_CONVERSION = YES;
318 | CLANG_WARN_COMMA = YES;
319 | CLANG_WARN_CONSTANT_CONVERSION = YES;
320 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
321 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
322 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
323 | CLANG_WARN_EMPTY_BODY = YES;
324 | CLANG_WARN_ENUM_CONVERSION = YES;
325 | CLANG_WARN_INFINITE_RECURSION = YES;
326 | CLANG_WARN_INT_CONVERSION = YES;
327 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
328 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
329 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
330 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
331 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
332 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
333 | CLANG_WARN_STRICT_PROTOTYPES = YES;
334 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
335 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
336 | CLANG_WARN_UNREACHABLE_CODE = YES;
337 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
338 | COPY_PHASE_STRIP = NO;
339 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
340 | ENABLE_NS_ASSERTIONS = NO;
341 | ENABLE_STRICT_OBJC_MSGSEND = YES;
342 | GCC_C_LANGUAGE_STANDARD = gnu11;
343 | GCC_NO_COMMON_BLOCKS = YES;
344 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
345 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
346 | GCC_WARN_UNDECLARED_SELECTOR = YES;
347 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
348 | GCC_WARN_UNUSED_FUNCTION = YES;
349 | GCC_WARN_UNUSED_VARIABLE = YES;
350 | IPHONEOS_DEPLOYMENT_TARGET = 12.0;
351 | MTL_ENABLE_DEBUG_INFO = NO;
352 | MTL_FAST_MATH = YES;
353 | SDKROOT = iphoneos;
354 | SWIFT_COMPILATION_MODE = wholemodule;
355 | SWIFT_OPTIMIZATION_LEVEL = "-O";
356 | VALIDATE_PRODUCT = YES;
357 | };
358 | name = Release;
359 | };
360 | 5C1ED05F26637CB000C68451 /* Debug */ = {
361 | isa = XCBuildConfiguration;
362 | baseConfigurationReference = D6FDACE8F7FE7BF61899FD4A /* Pods-ImpressionKitExample.debug.xcconfig */;
363 | buildSettings = {
364 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
365 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
366 | CODE_SIGN_IDENTITY = "Apple Development";
367 | CODE_SIGN_STYLE = Automatic;
368 | DEVELOPMENT_TEAM = 2X48A7XP4X;
369 | INFOPLIST_FILE = ImpressionKitExample/Info.plist;
370 | LD_RUNPATH_SEARCH_PATHS = (
371 | "$(inherited)",
372 | "@executable_path/Frameworks",
373 | );
374 | PRODUCT_BUNDLE_IDENTIFIER = com.yanni.demo;
375 | PRODUCT_NAME = "$(TARGET_NAME)";
376 | PROVISIONING_PROFILE_SPECIFIER = "";
377 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
378 | SUPPORTS_MACCATALYST = NO;
379 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
380 | SWIFT_VERSION = 5.0;
381 | TARGETED_DEVICE_FAMILY = 1;
382 | };
383 | name = Debug;
384 | };
385 | 5C1ED06026637CB000C68451 /* Release */ = {
386 | isa = XCBuildConfiguration;
387 | baseConfigurationReference = E93E5965889DA252ED3ECF35 /* Pods-ImpressionKitExample.release.xcconfig */;
388 | buildSettings = {
389 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
390 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
391 | CODE_SIGN_STYLE = Automatic;
392 | DEVELOPMENT_TEAM = Y79YM93K8M;
393 | INFOPLIST_FILE = ImpressionKitExample/Info.plist;
394 | LD_RUNPATH_SEARCH_PATHS = (
395 | "$(inherited)",
396 | "@executable_path/Frameworks",
397 | );
398 | PRODUCT_BUNDLE_IDENTIFIER = com.yanni.test;
399 | PRODUCT_NAME = "$(TARGET_NAME)";
400 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
401 | SUPPORTS_MACCATALYST = NO;
402 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
403 | SWIFT_VERSION = 5.0;
404 | TARGETED_DEVICE_FAMILY = 1;
405 | };
406 | name = Release;
407 | };
408 | /* End XCBuildConfiguration section */
409 |
410 | /* Begin XCConfigurationList section */
411 | 5C1ED04526637CB000C68451 /* Build configuration list for PBXProject "ImpressionKitExample" */ = {
412 | isa = XCConfigurationList;
413 | buildConfigurations = (
414 | 5C1ED05C26637CB000C68451 /* Debug */,
415 | 5C1ED05D26637CB000C68451 /* Release */,
416 | );
417 | defaultConfigurationIsVisible = 0;
418 | defaultConfigurationName = Release;
419 | };
420 | 5C1ED05E26637CB000C68451 /* Build configuration list for PBXNativeTarget "ImpressionKitExample" */ = {
421 | isa = XCConfigurationList;
422 | buildConfigurations = (
423 | 5C1ED05F26637CB000C68451 /* Debug */,
424 | 5C1ED06026637CB000C68451 /* Release */,
425 | );
426 | defaultConfigurationIsVisible = 0;
427 | defaultConfigurationName = Release;
428 | };
429 | /* End XCConfigurationList section */
430 | };
431 | rootObject = 5C1ED04226637CB000C68451 /* Project object */;
432 | }
433 |
--------------------------------------------------------------------------------