From 7f10065e5a333c48a1f2c6ee9b42c2461e7a77b6 Mon Sep 17 00:00:00 2001 From: Liu Lantao Date: Mon, 20 Nov 2017 07:24:20 +0800 Subject: [PATCH] TouchCanvas: Version 2.1, 2017-11-16 Modernize Swift. Signed-off-by: Liu Lantao --- TouchCanvas/.gitignore | 43 ++ TouchCanvas/LICENSE.txt | 42 ++ TouchCanvas/README.md | 26 ++ .../TouchCanvas.xcodeproj/project.pbxproj | 333 +++++++++++++++ TouchCanvas/TouchCanvas/AppDelegate.swift | 15 + .../AppIcon.appiconset/Contents.json | 68 ++++ .../Base.lproj/LaunchScreen.storyboard | 68 ++++ .../TouchCanvas/Base.lproj/Main.storyboard | 80 ++++ TouchCanvas/TouchCanvas/CanvasView.swift | 284 +++++++++++++ TouchCanvas/TouchCanvas/Info.plist | 47 +++ TouchCanvas/TouchCanvas/Line.swift | 379 ++++++++++++++++++ TouchCanvas/TouchCanvas/ReticleView.swift | 195 +++++++++ TouchCanvas/TouchCanvas/ViewController.swift | 149 +++++++ 13 files changed, 1729 insertions(+) create mode 100644 TouchCanvas/.gitignore create mode 100644 TouchCanvas/LICENSE.txt create mode 100644 TouchCanvas/README.md create mode 100644 TouchCanvas/TouchCanvas.xcodeproj/project.pbxproj create mode 100644 TouchCanvas/TouchCanvas/AppDelegate.swift create mode 100644 TouchCanvas/TouchCanvas/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 TouchCanvas/TouchCanvas/Base.lproj/LaunchScreen.storyboard create mode 100644 TouchCanvas/TouchCanvas/Base.lproj/Main.storyboard create mode 100644 TouchCanvas/TouchCanvas/CanvasView.swift create mode 100644 TouchCanvas/TouchCanvas/Info.plist create mode 100644 TouchCanvas/TouchCanvas/Line.swift create mode 100644 TouchCanvas/TouchCanvas/ReticleView.swift create mode 100644 TouchCanvas/TouchCanvas/ViewController.swift diff --git a/TouchCanvas/.gitignore b/TouchCanvas/.gitignore new file mode 100644 index 00000000..875ca7d7 --- /dev/null +++ b/TouchCanvas/.gitignore @@ -0,0 +1,43 @@ +.DS_Store + +# Xcode +build/* +*/build/* +*/**/build/* +*.mode1 +*.pbxuser +*.perspective +!default.perspectivev3 +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +*.xcworkspace +!default.xcworkspace +xcuserdata +profile +*.moved-aside + +# Generated files +VersionX-revision.h + +# build products +build/ +*.[oa] + +# version control files +.hg +.svn +CVS + +# automatic backup files +*~.nib +*.swp +*~ +*(Autosaved).rtfd/ +Backup[ ]of[ ]*.pages/ +Backup[ ]of[ ]*.key/ +Backup[ ]of[ ]*.numbers/ diff --git a/TouchCanvas/LICENSE.txt b/TouchCanvas/LICENSE.txt new file mode 100644 index 00000000..3eb8dbc7 --- /dev/null +++ b/TouchCanvas/LICENSE.txt @@ -0,0 +1,42 @@ +Sample code project: TouchCanvas: Using UITouch efficiently and effectively +Version: 2.1 + +IMPORTANT: This Apple software is supplied to you by Apple +Inc. ("Apple") in consideration of your agreement to the following +terms, and your use, installation, modification or redistribution of +this Apple software constitutes acceptance of these terms. If you do +not agree with these terms, please do not use, install, modify or +redistribute this Apple software. + +In consideration of your agreement to abide by the following terms, and +subject to these terms, Apple grants you a personal, non-exclusive +license, under Apple's copyrights in this original Apple software (the +"Apple Software"), to use, reproduce, modify and redistribute the Apple +Software, with or without modifications, in source and/or binary forms; +provided that if you redistribute the Apple Software in its entirety and +without modifications, you must retain this notice and the following +text and disclaimers in all such redistributions of the Apple Software. +Neither the name, trademarks, service marks or logos of Apple Inc. may +be used to endorse or promote products derived from the Apple Software +without specific prior written permission from Apple. Except as +expressly stated in this notice, no other rights or licenses, express or +implied, are granted by Apple herein, including but not limited to any +patent rights that may be infringed by your derivative works or by other +works in which the Apple Software may be incorporated. + +The Apple Software is provided by Apple on an "AS IS" basis. APPLE +MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION +THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND +OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. + +IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL +OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, +MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED +AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), +STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +Copyright (C) 2017 Apple Inc. All Rights Reserved. diff --git a/TouchCanvas/README.md b/TouchCanvas/README.md new file mode 100644 index 00000000..bc88ce2c --- /dev/null +++ b/TouchCanvas/README.md @@ -0,0 +1,26 @@ +# TouchCanvas: Using UITouch efficiently and effectively + +TouchCanvas illustrates responsive touch handling using coalesced and predictive touches (when available) via a simple drawing app. The sample uses force information (when available) to change line thickness. Apple Pencil and finger touches are distinguished via different colors. In addition, Apple Pencil only data is demonstrated through the use of estimated properties and updates providing the actual property data including the azimuth and altitude of the Apple Pencil while in use. + +The sample includes a debug and precise options as follows: + +* precise will force the use of UITouch's preciseLocation method over UITouch's location method for retrieving more precise screen coordinates. +* debug will draw the lines displayed in different colors depending on properties gathered from the UITouch APIs. See the enclosed LinePoint class in Line.swift. + +## Requirements + +### Build + +Xcode 9.0 or later, iOS 10.0 SDK or later + +### Runtime + +iOS 9.1 + +### Changes + +version 2.1 - updated to Swift 4.0 +version 2.0 - first release + +Copyright (C) 2017 Apple Inc. All rights reserved. + diff --git a/TouchCanvas/TouchCanvas.xcodeproj/project.pbxproj b/TouchCanvas/TouchCanvas.xcodeproj/project.pbxproj new file mode 100644 index 00000000..de763742 --- /dev/null +++ b/TouchCanvas/TouchCanvas.xcodeproj/project.pbxproj @@ -0,0 +1,333 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 164354CE1B8F816A004AEC75 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 164354CD1B8F816A004AEC75 /* AppDelegate.swift */; }; + 164354D01B8F816A004AEC75 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 164354CF1B8F816A004AEC75 /* ViewController.swift */; }; + 164354D31B8F816A004AEC75 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 164354D11B8F816A004AEC75 /* Main.storyboard */; }; + 164354D51B8F816A004AEC75 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 164354D41B8F816A004AEC75 /* Assets.xcassets */; }; + 164354D81B8F816A004AEC75 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 164354D61B8F816A004AEC75 /* LaunchScreen.storyboard */; }; + 169961181B8F82810003605A /* Line.swift in Sources */ = {isa = PBXBuildFile; fileRef = 169961161B8F82810003605A /* Line.swift */; }; + 169961191B8F82810003605A /* CanvasView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 169961171B8F82810003605A /* CanvasView.swift */; }; + 16D6FEAB1B97CD0100A2D1E6 /* ReticleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16D6FEAA1B97CD0100A2D1E6 /* ReticleView.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 164354CA1B8F816A004AEC75 /* TouchCanvas.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TouchCanvas.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 164354CD1B8F816A004AEC75 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 164354CF1B8F816A004AEC75 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 164354D21B8F816A004AEC75 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 164354D41B8F816A004AEC75 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 164354D71B8F816A004AEC75 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 164354D91B8F816A004AEC75 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 1647561B1B9921060039F559 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 169961161B8F82810003605A /* Line.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Line.swift; sourceTree = ""; }; + 169961171B8F82810003605A /* CanvasView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CanvasView.swift; sourceTree = ""; }; + 16D6FEAA1B97CD0100A2D1E6 /* ReticleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReticleView.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 164354C71B8F816A004AEC75 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 164354C11B8F816A004AEC75 = { + isa = PBXGroup; + children = ( + 1647561B1B9921060039F559 /* README.md */, + 164354CC1B8F816A004AEC75 /* TouchCanvas */, + 164354CB1B8F816A004AEC75 /* Products */, + ); + sourceTree = ""; + }; + 164354CB1B8F816A004AEC75 /* Products */ = { + isa = PBXGroup; + children = ( + 164354CA1B8F816A004AEC75 /* TouchCanvas.app */, + ); + name = Products; + sourceTree = ""; + }; + 164354CC1B8F816A004AEC75 /* TouchCanvas */ = { + isa = PBXGroup; + children = ( + 164354D11B8F816A004AEC75 /* Main.storyboard */, + 164354D61B8F816A004AEC75 /* LaunchScreen.storyboard */, + 164354CD1B8F816A004AEC75 /* AppDelegate.swift */, + 164354CF1B8F816A004AEC75 /* ViewController.swift */, + 169961171B8F82810003605A /* CanvasView.swift */, + 16D6FEAA1B97CD0100A2D1E6 /* ReticleView.swift */, + 169961161B8F82810003605A /* Line.swift */, + 164354D41B8F816A004AEC75 /* Assets.xcassets */, + 164354D91B8F816A004AEC75 /* Info.plist */, + ); + path = TouchCanvas; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 164354C91B8F816A004AEC75 /* TouchCanvas */ = { + isa = PBXNativeTarget; + buildConfigurationList = 164354DC1B8F816A004AEC75 /* Build configuration list for PBXNativeTarget "TouchCanvas" */; + buildPhases = ( + 164354C61B8F816A004AEC75 /* Sources */, + 164354C71B8F816A004AEC75 /* Frameworks */, + 164354C81B8F816A004AEC75 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = TouchCanvas; + productName = TouchCanvas; + productReference = 164354CA1B8F816A004AEC75 /* TouchCanvas.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 164354C21B8F816A004AEC75 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0710; + LastUpgradeCheck = 0900; + ORGANIZATIONNAME = "Apple, Inc."; + TargetAttributes = { + 164354C91B8F816A004AEC75 = { + CreatedOnToolsVersion = 7.0; + DevelopmentTeam = 8QXX5HQF6B; + LastSwiftMigration = 0800; + }; + }; + }; + buildConfigurationList = 164354C51B8F816A004AEC75 /* Build configuration list for PBXProject "TouchCanvas" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 164354C11B8F816A004AEC75; + productRefGroup = 164354CB1B8F816A004AEC75 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 164354C91B8F816A004AEC75 /* TouchCanvas */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 164354C81B8F816A004AEC75 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 164354D81B8F816A004AEC75 /* LaunchScreen.storyboard in Resources */, + 164354D51B8F816A004AEC75 /* Assets.xcassets in Resources */, + 164354D31B8F816A004AEC75 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 164354C61B8F816A004AEC75 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 169961181B8F82810003605A /* Line.swift in Sources */, + 16D6FEAB1B97CD0100A2D1E6 /* ReticleView.swift in Sources */, + 164354D01B8F816A004AEC75 /* ViewController.swift in Sources */, + 164354CE1B8F816A004AEC75 /* AppDelegate.swift in Sources */, + 169961191B8F82810003605A /* CanvasView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 164354D11B8F816A004AEC75 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 164354D21B8F816A004AEC75 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 164354D61B8F816A004AEC75 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 164354D71B8F816A004AEC75 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 164354DA1B8F816A004AEC75 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.1; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 164354DB1B8F816A004AEC75 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.1; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 164354DD1B8F816A004AEC75 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = 8QXX5HQF6B; + INFOPLIST_FILE = TouchCanvas/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.TouchCanvas"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + }; + name = Debug; + }; + 164354DE1B8F816A004AEC75 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = 8QXX5HQF6B; + INFOPLIST_FILE = TouchCanvas/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.TouchCanvas"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 164354C51B8F816A004AEC75 /* Build configuration list for PBXProject "TouchCanvas" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 164354DA1B8F816A004AEC75 /* Debug */, + 164354DB1B8F816A004AEC75 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 164354DC1B8F816A004AEC75 /* Build configuration list for PBXNativeTarget "TouchCanvas" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 164354DD1B8F816A004AEC75 /* Debug */, + 164354DE1B8F816A004AEC75 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 164354C21B8F816A004AEC75 /* Project object */; +} diff --git a/TouchCanvas/TouchCanvas/AppDelegate.swift b/TouchCanvas/TouchCanvas/AppDelegate.swift new file mode 100644 index 00000000..ed7599c1 --- /dev/null +++ b/TouchCanvas/TouchCanvas/AppDelegate.swift @@ -0,0 +1,15 @@ +/* + Copyright (C) 2017 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The application delegate. +*/ + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? +} \ No newline at end of file diff --git a/TouchCanvas/TouchCanvas/Assets.xcassets/AppIcon.appiconset/Contents.json b/TouchCanvas/TouchCanvas/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..36d2c80d --- /dev/null +++ b/TouchCanvas/TouchCanvas/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/TouchCanvas/TouchCanvas/Base.lproj/LaunchScreen.storyboard b/TouchCanvas/TouchCanvas/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..bddbde3c --- /dev/null +++ b/TouchCanvas/TouchCanvas/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TouchCanvas/TouchCanvas/Base.lproj/Main.storyboard b/TouchCanvas/TouchCanvas/Base.lproj/Main.storyboard new file mode 100644 index 00000000..b1aeb90c --- /dev/null +++ b/TouchCanvas/TouchCanvas/Base.lproj/Main.storyboard @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TouchCanvas/TouchCanvas/CanvasView.swift b/TouchCanvas/TouchCanvas/CanvasView.swift new file mode 100644 index 00000000..60b2357d --- /dev/null +++ b/TouchCanvas/TouchCanvas/CanvasView.swift @@ -0,0 +1,284 @@ +/* + Copyright (C) 2017 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `CanvasView` tracks `UITouch`es and represents them as a series of `Line`s. + */ + +import UIKit + +class CanvasView: UIView { + // MARK: Properties + + let isPredictionEnabled = UIDevice.current.userInterfaceIdiom == .pad + let isTouchUpdatingEnabled = true + + var usePreciseLocations = false { + didSet { + needsFullRedraw = true + setNeedsDisplay() + } + } + var isDebuggingEnabled = false { + didSet { + needsFullRedraw = true + setNeedsDisplay() + } + } + var needsFullRedraw = true + + /// Array containing all line objects that need to be drawn in `drawRect(_:)`. + var lines = [Line]() + + /// Array containing all line objects that have been completely drawn into the frozenContext. + var finishedLines = [Line]() + + + /** + Holds a map of `UITouch` objects to `Line` objects whose touch has not ended yet. + + Use `NSMapTable` to handle association as `UITouch` doesn't conform to `NSCopying`. There is no value + in accessing the properties of the touch used as a key in the map table. `UITouch` properties should + be accessed in `NSResponder` callbacks and methods called from them. + */ + let activeLines: NSMapTable = NSMapTable.strongToStrongObjects() + + /** + Holds a map of `UITouch` objects to `Line` objects whose touch has ended but still has points awaiting + updates. + + Use `NSMapTable` to handle association as `UITouch` doesn't conform to `NSCopying`. There is no value + in accessing the properties of the touch used as a key in the map table. `UITouch` properties should + be accessed in `NSResponder` callbacks and methods called from them. + */ + let pendingLines: NSMapTable = NSMapTable.strongToStrongObjects() + + /// A `CGContext` for drawing the last representation of lines no longer receiving updates into. + lazy var frozenContext: CGContext = { + let scale = self.window!.screen.scale + var size = self.bounds.size + + size.width *= scale + size.height *= scale + let colorSpace = CGColorSpaceCreateDeviceRGB() + + let context: CGContext = CGContext.init(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)! + + context.setLineCap(.round) + let transform = CGAffineTransform.init(scaleX:scale, y: scale) + context.concatenate(transform) + + return context + }() + + /// An optional `CGImage` containing the last representation of lines no longer receiving updates. + var frozenImage: CGImage? + + // MARK: Drawing + + override func draw(_ rect: CGRect) { + let context = UIGraphicsGetCurrentContext()! + + context.setLineCap(.round) + + if needsFullRedraw { + setFrozenImageNeedsUpdate() + frozenContext.clear(bounds) + for array in [finishedLines,lines] { + for line in array { + line.drawCommitedPointsInContext(context: frozenContext, isDebuggingEnabled: isDebuggingEnabled, usePreciseLocation: usePreciseLocations) + } + } + needsFullRedraw = false + } + + frozenImage = frozenImage ?? frozenContext.makeImage() + + if let frozenImage = frozenImage { + context.draw(frozenImage, in: bounds) + } + + for line in lines { + line.drawInContext(context: context, isDebuggingEnabled: isDebuggingEnabled, usePreciseLocation: usePreciseLocations) + } + } + + func setFrozenImageNeedsUpdate() { + frozenImage = nil + } + + // MARK: Actions + + func clear() { + activeLines.removeAllObjects() + pendingLines.removeAllObjects() + lines.removeAll() + finishedLines.removeAll() + needsFullRedraw = true + setNeedsDisplay() + } + + // MARK: Convenience + + func drawTouches(touches: Set, withEvent event: UIEvent?) { + var updateRect = CGRect.null + + for touch in touches { + // Retrieve a line from `activeLines`. If no line exists, create one. + let line: Line = activeLines.object(forKey: touch) ?? addActiveLineForTouch(touch: touch) + + /* + Remove prior predicted points and update the `updateRect` based on the removals. The touches + used to create these points are predictions provided to offer additional data. They are stale + by the time of the next event for this touch. + */ + updateRect = updateRect.union(line.removePointsWithType(type: .Predicted)) + + /* + Incorporate coalesced touch data. The data in the last touch in the returned array will match + the data of the touch supplied to `coalescedTouchesForTouch(_:)` + */ + let coalescedTouches = event?.coalescedTouches(for: touch) ?? [] + let coalescedRect = addPointsOfType(type: .Coalesced, forTouches: coalescedTouches, toLine: line, currentUpdateRect: updateRect) + updateRect = updateRect.union(coalescedRect) + + /* + Incorporate predicted touch data. This sample draws predicted touches differently; however, + you may want to use them as inputs to smoothing algorithms rather than directly drawing them. + Points derived from predicted touches should be removed from the line at the next event for + this touch. + */ + if isPredictionEnabled { + let predictedTouches = event?.predictedTouches(for: touch) ?? [] + let predictedRect = addPointsOfType(type: .Predicted, forTouches: predictedTouches, toLine: line, currentUpdateRect: updateRect) + updateRect = updateRect.union(predictedRect) + } + } + + setNeedsDisplay(updateRect) + } + + func addActiveLineForTouch(touch: UITouch) -> Line { + let newLine = Line() + + activeLines.setObject(newLine, forKey: touch) + + lines.append(newLine) + + return newLine + } + + func addPointsOfType(type: LinePoint.PointType, forTouches touches: [UITouch], toLine line: Line, currentUpdateRect updateRect: CGRect) -> CGRect { + var accumulatedRect = CGRect.null + var type = type + + for (idx, touch) in touches.enumerated() { + let isStylus = touch.type == .stylus + + // The visualization displays non-`.Stylus` touches differently. + if !isStylus { + type.formUnion(.Finger) + } + + // Touches with estimated properties require updates; add this information to the `PointType`. + if isTouchUpdatingEnabled && !touch.estimatedProperties.isEmpty { + type.formUnion(.NeedsUpdate) + } + + // The last touch in a set of `.Coalesced` touches is the originating touch. Track it differently. + if type.contains(.Coalesced) && idx == touches.count - 1 { + type.subtract(.Coalesced) + type.formUnion(.Standard) + } + + let touchRect = line.addPointOfType(pointType: type, forTouch: touch) + accumulatedRect = accumulatedRect.union(touchRect) + + commitLine(line: line) + } + + return updateRect.union(accumulatedRect) + } + + func endTouches(touches: Set, cancel: Bool) { + var updateRect = CGRect.null + + for touch in touches { + // Skip over touches that do not correspond to an active line. + guard let line = activeLines.object(forKey: touch) else { continue } + + // If this is a touch cancellation, cancel the associated line. + if cancel { updateRect = updateRect.union(line.cancel()) } + + // If the line is complete (no points needing updates) or updating isn't enabled, move the line to the `frozenImage`. + if line.isComplete || !isTouchUpdatingEnabled { + finishLine(line: line) + } + // Otherwise, add the line to our map of touches to lines pending update. + else { + pendingLines.setObject(line, forKey: touch) + } + + // This touch is ending, remove the line corresponding to it from `activeLines`. + activeLines.removeObject(forKey: touch) + } + + setNeedsDisplay(updateRect) + } + + func updateEstimatedPropertiesForTouches(touches: Set) { + guard isTouchUpdatingEnabled, let touches = touches as? Set else { return } + + for touch in touches { + var isPending = false + + // Look to retrieve a line from `activeLines`. If no line exists, look it up in `pendingLines`. + let possibleLine: Line? = activeLines.object(forKey: touch) ?? { + let pendingLine = pendingLines.object(forKey: touch) + isPending = pendingLine != nil + return pendingLine + }() + + // If no line is related to the touch, return as there is no additional work to do. + guard let line = possibleLine else { return } + + switch line.updateWithTouch(touch: touch) { + case (true, let updateRect): + setNeedsDisplay(updateRect) + default: + () + } + + // If this update updated the last point requiring an update, move the line to the `frozenImage`. + if isPending && line.isComplete { + finishLine(line: line) + pendingLines.removeObject(forKey: touch) + } + // Otherwise, have the line add any points no longer requiring updates to the `frozenImage`. + else { + commitLine(line: line) + } + + } + } + + func commitLine(line: Line) { + // Have the line draw any segments between points no longer being updated into the `frozenContext` and remove them from the line. + line.drawFixedPointsInContext(context: frozenContext, isDebuggingEnabled: isDebuggingEnabled, usePreciseLocation: usePreciseLocations) + setFrozenImageNeedsUpdate() + } + + func finishLine(line: Line) { + // Have the line draw any remaining segments into the `frozenContext`. All should be fixed now. + line.drawFixedPointsInContext(context: frozenContext, isDebuggingEnabled: isDebuggingEnabled, usePreciseLocation: usePreciseLocations, commitAll: true) + setFrozenImageNeedsUpdate() + + // Cease tracking this line now that it is finished. + lines.remove(at: lines.index(of: line)!) + + // Store into finished lines to allow for a full redraw on option changes. + finishedLines.append(line) + } +} + diff --git a/TouchCanvas/TouchCanvas/Info.plist b/TouchCanvas/TouchCanvas/Info.plist new file mode 100644 index 00000000..4de100a6 --- /dev/null +++ b/TouchCanvas/TouchCanvas/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 2.0 + CFBundleSignature + ???? + CFBundleVersion + 2.0 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UIRequiresFullScreen + + UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/TouchCanvas/TouchCanvas/Line.swift b/TouchCanvas/TouchCanvas/Line.swift new file mode 100644 index 00000000..0e389656 --- /dev/null +++ b/TouchCanvas/TouchCanvas/Line.swift @@ -0,0 +1,379 @@ +/* + Copyright (C) 2017 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Contains the `Line` and `LinePoint` types used to represent and draw lines derived from touches. + */ + +import UIKit + +class Line: NSObject { + // MARK: Properties + + // The live line. + var points = [LinePoint]() + + // Use the estimation index of the touch to track points awaiting updates. + var pointsWaitingForUpdatesByEstimationIndex = [NSNumber: LinePoint]() + + // Points already drawn into 'frozen' representation of this line. + var committedPoints = [LinePoint]() + + var isComplete: Bool { + return pointsWaitingForUpdatesByEstimationIndex.count == 0 + } + + func updateWithTouch(touch: UITouch) -> (Bool, CGRect) { + if let estimationUpdateIndex = touch.estimationUpdateIndex, + let point = pointsWaitingForUpdatesByEstimationIndex[estimationUpdateIndex] { + var rect = updateRectForExistingPoint(point: point) + let didUpdate = point.updateWithTouch(touch: touch) + if didUpdate { + rect = rect.union(updateRectForExistingPoint(point: point)) + } + if point.estimatedPropertiesExpectingUpdates == [] { + pointsWaitingForUpdatesByEstimationIndex.removeValue(forKey: estimationUpdateIndex) + } + return (didUpdate,rect) + } + return (false, CGRect.null) + } + + // MARK: Interface + + func addPointOfType(pointType: LinePoint.PointType, forTouch touch: UITouch) -> CGRect { + let previousPoint = points.last + let previousSequenceNumber = previousPoint?.sequenceNumber ?? -1 + let point = LinePoint(touch: touch, sequenceNumber: previousSequenceNumber + 1, pointType:pointType) + + if let estimationIndex = point.estimationUpdateIndex { + if !point.estimatedPropertiesExpectingUpdates.isEmpty { + pointsWaitingForUpdatesByEstimationIndex[estimationIndex] = point + } + } + + points.append(point) + + let updateRect = updateRectForLinePoint(point: point, previousPoint: previousPoint) + + return updateRect + } + + func removePointsWithType(type: LinePoint.PointType) -> CGRect { + var updateRect = CGRect.null + var priorPoint: LinePoint? + + points = points.filter { point in + let keepPoint = !point.pointType.contains(type) + + if !keepPoint { + var rect = self.updateRectForLinePoint(point: point) + + if let priorPoint = priorPoint { + rect = rect.union(updateRectForLinePoint(point: priorPoint)) + } + + updateRect = updateRect.union(rect) + } + + priorPoint = point + + return keepPoint + } + + return updateRect + } + + func cancel() -> CGRect { + // Process each point in the line and accumulate the `CGRect` containing all the points. + let updateRect = points.reduce(CGRect.null) { accumulated, point in + // Update the type set to include `.Cancelled`. + point.pointType.formUnion(.Cancelled) + + /* + Union the `CGRect` for this point with accumulated `CGRect` and return it. The result is + supplied to the next invocation of the closure. + */ + return accumulated.union(updateRectForLinePoint(point: point)) + } + + return updateRect + } + + // MARK: Drawing + + func drawInContext(context: CGContext, isDebuggingEnabled: Bool, usePreciseLocation: Bool) { + var maybePriorPoint: LinePoint? + + for point in points { + guard let priorPoint = maybePriorPoint else { + maybePriorPoint = point + continue + } + + // This color will used by default for `.Standard` touches. + var color = UIColor.black + + let pointType = point.pointType + if isDebuggingEnabled { + if pointType.contains(.Cancelled) { + color = UIColor.red + } + else if pointType.contains(.NeedsUpdate) { + color = UIColor.orange + } + else if pointType.contains(.Finger) { + color = UIColor.purple + } + else if pointType.contains(.Coalesced) { + color = UIColor.green + } + else if pointType.contains(.Predicted) { + color = UIColor.blue + } + } else { + if pointType.contains(.Cancelled) { + color = UIColor.clear + } + else if pointType.contains(.Finger) { + color = UIColor.purple + } + if pointType.contains(.Predicted) && !pointType.contains(.Cancelled) { + color = color.withAlphaComponent(0.5) + } + } + + let location = usePreciseLocation ? point.preciseLocation : point.location + let priorLocation = usePreciseLocation ? priorPoint.preciseLocation : priorPoint.location + + context.setStrokeColor(color.cgColor) + + context.beginPath() + + context.move(to: CGPoint(x: priorLocation.x, y: priorLocation.y)) + context.addLine(to: CGPoint(x: location.x, y: location.y)) + + context.setLineWidth(point.magnitude) + context.strokePath() + + // Draw azimuith and elevation on all non-coalesced points when debugging. + if isDebuggingEnabled && !pointType.contains(.Coalesced) && !pointType.contains(.Predicted) && !pointType.contains(.Finger) { + context.beginPath() + context.setStrokeColor(UIColor.red.cgColor) + context.setLineWidth(0.5) + context.move(to: CGPoint(x: location.x, y: location.y)) + var targetPoint = CGPoint(x: 0.5 + 10.0 * cos(point.altitudeAngle), y:0.0) + targetPoint = targetPoint.applying(CGAffineTransform.init(rotationAngle: point.azimuthAngle)) + targetPoint.x += location.x + targetPoint.y += location.y + context.addLine(to: CGPoint(x: targetPoint.x, y: targetPoint.y)) + context.strokePath() + } + + maybePriorPoint = point + } + } + + func drawFixedPointsInContext(context: CGContext, isDebuggingEnabled: Bool, usePreciseLocation: Bool, commitAll: Bool = false) { + let allPoints = points + var committing = [LinePoint]() + + if commitAll { + committing = allPoints + points.removeAll() + } + else { + for (index, point) in allPoints.enumerated() { + // Only points whose type does not include `.NeedsUpdate` or `.Predicted` and are not last or prior to last point can be committed. + guard point.pointType.intersection([.NeedsUpdate, .Predicted]).isEmpty && index < allPoints.count - 2 else { + committing.append(points.first!) + break + } + + guard index > 0 else { continue } + + // First time to this point should be index 1 if there is a line segment that can be committed. + let removed = points.removeFirst() + committing.append(removed) + } + } + // If only one point could be committed, no further action is required. Otherwise, draw the `committedLine`. + guard committing.count > 1 else { return } + + let committedLine = Line() + committedLine.points = committing + committedLine.drawInContext(context: context, isDebuggingEnabled: isDebuggingEnabled, usePreciseLocation: usePreciseLocation) + + + if committedPoints.count > 0 { + // Remove what was the last point committed point; it is also the first point being committed now. + committedPoints.removeLast() + } + + // Store the points being committed for redrawing later in a different style if needed. + committedPoints.append(contentsOf: committing) + } + + func drawCommitedPointsInContext(context: CGContext, isDebuggingEnabled: Bool, usePreciseLocation: Bool) { + let committedLine = Line() + committedLine.points = committedPoints + committedLine.drawInContext(context: context, isDebuggingEnabled: isDebuggingEnabled, usePreciseLocation: usePreciseLocation) + } + + // MARK: Convenience + + func updateRectForLinePoint(point: LinePoint) -> CGRect { + var rect = CGRect(origin: point.location, size: CGSize.zero) + + // The negative magnitude ensures an outset rectangle. + let magnitude = -3 * point.magnitude - 2 + rect = rect.insetBy(dx: magnitude, dy: magnitude) + + return rect + } + + func updateRectForLinePoint(point: LinePoint, previousPoint optionalPreviousPoint: LinePoint? = nil) -> CGRect { + var rect = CGRect(origin: point.location, size: CGSize.zero) + + var pointMagnitude = point.magnitude + + if let previousPoint = optionalPreviousPoint { + pointMagnitude = max(pointMagnitude, previousPoint.magnitude) + rect = rect.union( CGRect(origin: previousPoint.location, size: CGSize.zero)) + } + + // The negative magnitude ensures an outset rectangle. + let magnitude = -3.0 * pointMagnitude - 2.0 + rect = rect.insetBy(dx: magnitude, dy: magnitude) + + return rect + } + + func updateRectForExistingPoint(point: LinePoint) -> CGRect { + var rect = updateRectForLinePoint(point: point) + + let arrayIndex = point.sequenceNumber - points.first!.sequenceNumber + + if arrayIndex > 0 { + rect = rect.union(updateRectForLinePoint(point: point, previousPoint: points[arrayIndex-1])) + } + if arrayIndex + 1 < points.count { + rect = rect.union(updateRectForLinePoint(point: point, previousPoint: points[arrayIndex+1])) + } + return rect + } + +} + + +class LinePoint: NSObject { + // MARK: Types + + struct PointType: OptionSet { + // MARK: Properties + + let rawValue: Int + + // MARK: Options + + static var Standard: PointType { return self.init(rawValue: 0) } + static var Coalesced: PointType { return self.init(rawValue: 1 << 0) } + static var Predicted: PointType { return self.init(rawValue: 1 << 1) } + static var NeedsUpdate: PointType { return self.init(rawValue: 1 << 2) } + static var Updated: PointType { return self.init(rawValue: 1 << 3) } + static var Cancelled: PointType { return self.init(rawValue: 1 << 4) } + static var Finger: PointType { return self.init(rawValue: 1 << 5) } + } + + // MARK: Properties + + var sequenceNumber: Int + let timestamp: TimeInterval + var force: CGFloat + var location: CGPoint + var preciseLocation: CGPoint + var estimatedPropertiesExpectingUpdates: UITouchProperties + var estimatedProperties: UITouchProperties + let type: UITouchType + var altitudeAngle: CGFloat + var azimuthAngle: CGFloat + let estimationUpdateIndex: NSNumber? + + var pointType: PointType + + var magnitude: CGFloat { + return max(force, 0.025) + } + + // MARK: Initialization + + init(touch: UITouch, sequenceNumber: Int, pointType: PointType) { + self.sequenceNumber = sequenceNumber + self.type = touch.type + self.pointType = pointType + + timestamp = touch.timestamp + let view = touch.view + location = touch.location(in: view) + preciseLocation = touch.preciseLocation(in: view) + azimuthAngle = touch.azimuthAngle(in: view) + estimatedProperties = touch.estimatedProperties + estimatedPropertiesExpectingUpdates = touch.estimatedPropertiesExpectingUpdates + altitudeAngle = touch.altitudeAngle + force = (type == .stylus || touch.force > 0) ? touch.force : 1.0 + + if !estimatedPropertiesExpectingUpdates.isEmpty { + self.pointType.formUnion(.NeedsUpdate) + } + + estimationUpdateIndex = touch.estimationUpdateIndex + } + + func updateWithTouch(touch: UITouch) -> Bool { + guard let estimationUpdateIndex = touch.estimationUpdateIndex, estimationUpdateIndex == estimationUpdateIndex else { return false } + + // An array of the touch properties that may be of interest. + let touchProperties: [UITouchProperties] = [.altitude, .azimuth, .force, .location] + + // Iterate through possible properties. + for expectedProperty in touchProperties { + // If an update to this property is not expected, continue to the next property. + guard !estimatedPropertiesExpectingUpdates.contains(expectedProperty) else { continue } + + // Update the value of the point with the value from the touch's property. + switch expectedProperty { + case UITouchProperties.force: + force = touch.force + case UITouchProperties.azimuth: + azimuthAngle = touch.azimuthAngle(in: touch.view) + case UITouchProperties.altitude: + altitudeAngle = touch.altitudeAngle + case UITouchProperties.location: + location = touch.location(in: touch.view) + preciseLocation = touch.preciseLocation(in: touch.view) + default: + () + } + + if !touch.estimatedProperties.contains(expectedProperty) { + // Flag that this point now has a 'final' value for this property. + estimatedProperties.subtract(expectedProperty) + } + + if !touch.estimatedPropertiesExpectingUpdates.contains(expectedProperty) { + // Flag that this point is no longer expecting updates for this property. + estimatedPropertiesExpectingUpdates.subtract(expectedProperty) + + if estimatedPropertiesExpectingUpdates.isEmpty { + // Flag that this point has been updated and no longer needs updates. + pointType.subtract(.NeedsUpdate) + pointType.formUnion(.Updated) + } + } + } + + return true + } +} + diff --git a/TouchCanvas/TouchCanvas/ReticleView.swift b/TouchCanvas/TouchCanvas/ReticleView.swift new file mode 100644 index 00000000..213e21c3 --- /dev/null +++ b/TouchCanvas/TouchCanvas/ReticleView.swift @@ -0,0 +1,195 @@ +/* + Copyright (C) 2017 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `ReticleView` allows visualization of the azimuth and altitude related properties of a `UITouch` via an indicator similar to the sighting devices such as a telescope. + */ + +import UIKit + +class ReticleView: UIView { + // MARK: Properties + + var actualAzimuthAngle: CGFloat = 0.0 { + didSet { + setNeedsLayout() + } + } + var actualAzimuthUnitVector = CGVector(dx: 0, dy: 0) { + didSet { + setNeedsLayout() + } + } + var actualAltitudeAngle: CGFloat = 0.0 { + didSet { + setNeedsLayout() + } + } + + var predictedAzimuthAngle: CGFloat = 0.0 { + didSet { + setNeedsLayout() + } + } + var predictedAzimuthUnitVector = CGVector(dx: 0, dy: 0) { + didSet { + setNeedsLayout() + } + } + var predictedAltitudeAngle: CGFloat = 0.0 { + didSet { + setNeedsLayout() + } + } + + let reticleLayer = CALayer() + let radius: CGFloat = 80 + var reticleImage: UIImage! + let reticleColor = UIColor(hue: 0.516, saturation: 0.38, brightness: 0.85, alpha: 0.4) + + let dotRadius: CGFloat = 8 + let lineWidth: CGFloat = 2 + + var predictedDotLayer = CALayer() + var predictedLineLayer = CALayer() + let predictedIndicatorColor = UIColor(hue: 0.53, saturation: 0.86, brightness: 0.91, alpha: 1.0) + + var dotLayer = CALayer() + var lineLayer = CALayer() + let indicatorColor = UIColor(hue: 0.0, saturation: 0.86, brightness: 0.91, alpha: 1.0) + + // MARK: Initialization + + override init(frame: CGRect) { + super.init(frame: frame) + + // Set the contentScaleFactor. + contentScaleFactor = UIScreen.main.scale + + reticleLayer.contentsGravity = kCAGravityCenter + reticleLayer.position = layer.position + layer.addSublayer(reticleLayer) + + configureDotLayer(layer: predictedDotLayer, withColor: predictedIndicatorColor) + predictedDotLayer.isHidden = true + configureLineLayer(layer: predictedLineLayer, withColor: predictedIndicatorColor) + predictedLineLayer.isHidden = true + + configureDotLayer(layer: dotLayer, withColor: indicatorColor) + configureLineLayer(layer: lineLayer, withColor: indicatorColor) + + reticleLayer.addSublayer(predictedDotLayer) + reticleLayer.addSublayer(predictedLineLayer) + reticleLayer.addSublayer(dotLayer) + reticleLayer.addSublayer(lineLayer) + + renderReticleImage() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: UIView Overrides + + override var intrinsicContentSize: CGSize { + get { return reticleImage.size } + + } + + override func layoutSubviews() { + super.layoutSubviews() + + CATransaction.setDisableActions(true) + + reticleLayer.position = CGPoint(x: bounds.size.width / 2, y: bounds.size.height / 2) + layoutIndicator() + + CATransaction.setDisableActions(false) + } + + // MARK: Convenience + + func renderReticleImage() { + let imageRadius = ceil(radius * 1.2) + let imageSize = CGSize(width: imageRadius * 2, height: imageRadius * 2) + UIGraphicsBeginImageContextWithOptions(imageSize, false, contentScaleFactor) + let ctx: CGContext = UIGraphicsGetCurrentContext()! + ctx.translateBy(x:imageRadius, y:imageRadius) + ctx.setLineWidth(2.0) + ctx.setStrokeColor(reticleColor.cgColor) + ctx.strokeEllipse(in: CGRect(x: -radius, y: -radius, width: radius * 2, height: radius * 2)) + + // Draw targeting lines. + let path = CGMutablePath.init() + var transform = CGAffineTransform.identity + + + for _ in 0..<4 { + path.move(to: CGPoint(x: radius * 0.5, y: 0), transform: transform) + path.addLine(to: CGPoint(x: radius * 1.15, y: 0), transform: transform) + transform = transform.rotated(by: CGFloat.pi) + } + ctx.addPath(path) + ctx.strokePath() + + reticleImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + reticleLayer.contents = reticleImage.cgImage + reticleLayer.bounds = CGRect(x: 0, y: 0, width: imageRadius * 2, height: imageRadius * 2) + reticleLayer.contentsScale = contentScaleFactor + } + + func layoutIndicator() { + // Predicted. + layoutIndicatorForAzimuthAngle(azimuthAngle: predictedAzimuthAngle, azimuthUnitVector: predictedAzimuthUnitVector, altitudeAngle: predictedAltitudeAngle, lineLayer: predictedLineLayer, dotLayer: predictedDotLayer) + + // Actual. + layoutIndicatorForAzimuthAngle(azimuthAngle: actualAzimuthAngle, azimuthUnitVector: actualAzimuthUnitVector, altitudeAngle: actualAltitudeAngle, lineLayer: lineLayer, dotLayer: dotLayer) + } + + func layoutIndicatorForAzimuthAngle(azimuthAngle: CGFloat, azimuthUnitVector: CGVector, altitudeAngle: CGFloat, lineLayer targetLineLayer: CALayer, dotLayer targetDotLayer: CALayer) { + let reticleBounds = reticleLayer.bounds + + let centeringTransform = CGAffineTransform.init(translationX: reticleBounds.width / 2, y:reticleBounds.height / 2) + + var rotationTransform = CGAffineTransform.init(rotationAngle:azimuthAngle) + + // Draw the indicator opposite the azimuth by rotating pi radians, for easy visualization. + rotationTransform = rotationTransform.rotated(by: CGFloat.pi) + + /* + Make the length of the indicator's line representative of the `altitudeAngle`. When the angle is + zero radians (parallel to the screen surface) the line will be at its longest. At `M_PI`/2 radians, + only the dot on top of the indicator will be visible directly beneath the touch location. + */ + let altitudeRadius = (1.0 - altitudeAngle / CGFloat.pi) * radius + + var lineTransform = CGAffineTransform.init(scaleX: altitudeRadius, y: 1) + lineTransform = lineTransform.concatenating(rotationTransform) + lineTransform = lineTransform.concatenating(centeringTransform) + targetLineLayer.setAffineTransform(lineTransform) + + var dotTransform = CGAffineTransform.init(translationX: -azimuthUnitVector.dx * altitudeRadius, y: -azimuthUnitVector.dy * altitudeRadius) + dotTransform = dotTransform.concatenating(centeringTransform) + + targetDotLayer.setAffineTransform(dotTransform) + } + + func configureDotLayer(layer targetLayer: CALayer, withColor color: UIColor) { + targetLayer.backgroundColor = color.cgColor + targetLayer.bounds = CGRect(x: 0, y: 0, width: dotRadius * 2, height: dotRadius * 2) + targetLayer.cornerRadius = dotRadius + targetLayer.position = CGPoint.zero + } + + func configureLineLayer(layer targetLayer: CALayer, withColor color: UIColor) { + targetLayer.backgroundColor = color.cgColor + targetLayer.bounds = CGRect(x: 0, y: 0, width: 1, height: lineWidth) + targetLayer.anchorPoint = CGPoint(x: 0, y: 0.5) + targetLayer.position = CGPoint.zero + } +} + diff --git a/TouchCanvas/TouchCanvas/ViewController.swift b/TouchCanvas/TouchCanvas/ViewController.swift new file mode 100644 index 00000000..9b1b399a --- /dev/null +++ b/TouchCanvas/TouchCanvas/ViewController.swift @@ -0,0 +1,149 @@ +/* + Copyright (C) 2017 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The primary view controller that hosts a `CanvasView` for the user to interact with. + */ + +import UIKit + +class ViewController: UIViewController { + // MARK: Properties + + var visualizeAzimuth = false + + let reticleView: ReticleView = { + let view = ReticleView(frame: CGRect.null) + view.translatesAutoresizingMaskIntoConstraints = false + view.isHidden = true + + return view + }() + + var canvasView: CanvasView { + return view as! CanvasView + } + + // MARK: View Life Cycle + + override func viewDidLoad() { + canvasView.addSubview(reticleView) + } + + // MARK: Touch Handling + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + canvasView.drawTouches(touches: touches, withEvent: event) + + if visualizeAzimuth { + for touch in touches { + if touch.type == .stylus { + reticleView.isHidden = false + updateReticleViewWithTouch(touch: touch, event: event) + } + } + } + } + + override func touchesMoved(_ touches: Set, with event: UIEvent?) { + canvasView.drawTouches(touches: touches, withEvent: event) + + if visualizeAzimuth { + for touch in touches { + if touch.type == .stylus { + updateReticleViewWithTouch(touch: touch, event: event) + + // Use the last predicted touch to update the reticle. + guard let predictedTouch = event?.predictedTouches(for: touch)?.last else { return } + + updateReticleViewWithTouch(touch: predictedTouch, event: event, isPredicted: true) + } + } + } + } + + override func touchesEnded(_ touches: Set, with event: UIEvent?) { + canvasView.drawTouches(touches: touches, withEvent: event) + canvasView.endTouches(touches: touches, cancel: false) + + if visualizeAzimuth { + for touch in touches { + if touch.type == .stylus { + reticleView.isHidden = true + } + } + } + } + + override func touchesCancelled(_ touches: Set, with event: UIEvent?) { + canvasView.endTouches(touches: touches, cancel: true) + + if visualizeAzimuth { + for touch in touches { + if touch.type == .stylus { + reticleView.isHidden = true + } + } + } + } + + override func touchesEstimatedPropertiesUpdated(_ touches: Set) { + canvasView.updateEstimatedPropertiesForTouches(touches: touches) + } + + // MARK: Actions + + @IBAction func clearView(sender: UIBarButtonItem) { + canvasView.clear() + } + + @IBAction func toggleDebugDrawing(sender: UIButton) { + canvasView.isDebuggingEnabled = !canvasView.isDebuggingEnabled + visualizeAzimuth = !visualizeAzimuth + sender.isSelected = canvasView.isDebuggingEnabled + } + + @IBAction func toggleUsePreciseLocations(sender: UIButton) { + canvasView.usePreciseLocations = !canvasView.usePreciseLocations + sender.isSelected = canvasView.usePreciseLocations + } + + // MARK: Rotation + + override var shouldAutorotate: Bool { + get { return true } + } + + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + get { return [.landscapeLeft, .landscapeRight] } + } + + + // MARK: Convenience + + func updateReticleViewWithTouch(touch: UITouch?, event: UIEvent?, isPredicted: Bool = false) { + guard let touch = touch, touch.type == .stylus else { return } + + reticleView.predictedDotLayer.isHidden = !isPredicted + reticleView.predictedLineLayer.isHidden = !isPredicted + + let azimuthAngle = touch.azimuthAngle(in: view) + let azimuthUnitVector = touch.azimuthUnitVector(in: view) + let altitudeAngle = touch.altitudeAngle + + if isPredicted { + reticleView.predictedAzimuthAngle = azimuthAngle + reticleView.predictedAzimuthUnitVector = azimuthUnitVector + reticleView.predictedAltitudeAngle = altitudeAngle + } + else { + let location = touch.preciseLocation(in: view) + reticleView.center = location + reticleView.actualAzimuthAngle = azimuthAngle + reticleView.actualAzimuthUnitVector = azimuthUnitVector + reticleView.actualAltitudeAngle = altitudeAngle + } + } +} +