TouchCanvas: Version 2.1, 2017-11-16

Modernize Swift.

Signed-off-by: Liu Lantao <liulantao@gmail.com>
This commit is contained in:
Liu Lantao 2017-11-20 07:24:20 +08:00
parent 84042880ef
commit 7f10065e5a
No known key found for this signature in database
GPG Key ID: BF35AA0CD375679D
13 changed files with 1729 additions and 0 deletions

43
TouchCanvas/.gitignore vendored Normal file
View File

@ -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/

42
TouchCanvas/LICENSE.txt Normal file
View File

@ -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.

26
TouchCanvas/README.md Normal file
View File

@ -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.

View File

@ -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 = "<group>"; };
164354CF1B8F816A004AEC75 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
164354D21B8F816A004AEC75 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
164354D41B8F816A004AEC75 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
164354D71B8F816A004AEC75 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
164354D91B8F816A004AEC75 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
1647561B1B9921060039F559 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
169961161B8F82810003605A /* Line.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Line.swift; sourceTree = "<group>"; };
169961171B8F82810003605A /* CanvasView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CanvasView.swift; sourceTree = "<group>"; };
16D6FEAA1B97CD0100A2D1E6 /* ReticleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReticleView.swift; sourceTree = "<group>"; };
/* 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 = "<group>";
};
164354CB1B8F816A004AEC75 /* Products */ = {
isa = PBXGroup;
children = (
164354CA1B8F816A004AEC75 /* TouchCanvas.app */,
);
name = Products;
sourceTree = "<group>";
};
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 = "<group>";
};
/* 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 = "<group>";
};
164354D61B8F816A004AEC75 /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
164354D71B8F816A004AEC75 /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* 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 */;
}

View File

@ -0,0 +1,15 @@
/*
Copyright (C) 2017 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
The application delegate.
*/
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
}

View File

@ -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"
}
}

View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="9053" systemVersion="15B22" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="7ma-fb-sCB">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="9042"/>
<capability name="Navigation items with more than one left or right bar item" minToolsVersion="7.0"/>
</dependencies>
<scenes>
<!--Touch Canvas-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
<viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<animations/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
<navigationItem key="navigationItem" title="Touch Canvas" id="9Cy-io-HlN">
<leftBarButtonItems>
<barButtonItem id="Jj7-5O-zca">
<button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" id="eTA-U8-P0P">
<rect key="frame" x="20" y="7" width="46" height="30"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<animations/>
<state key="normal" title="Debug"/>
</button>
</barButtonItem>
<barButtonItem id="9yd-FW-pCi">
<button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" id="DHJ-dJ-uEC">
<rect key="frame" x="74" y="7" width="51" height="30"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<animations/>
<state key="normal" title="Precise"/>
</button>
</barButtonItem>
</leftBarButtonItems>
<barButtonItem key="rightBarButtonItem" systemItem="trash" id="dE4-O8-V7y"/>
</navigationItem>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="865" y="375"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="uEg-a7-k5Q">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="7ma-fb-sCB" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="e7o-hd-Iol">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<animations/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="01J-lp-oVM" kind="relationship" relationship="rootViewController" id="bOU-lT-8GO"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="6EZ-vu-EsO" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13196" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="3pu-ol-7ZX">
<device id="ipad9_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13173"/>
<capability name="Navigation items with more than one left or right bar item" minToolsVersion="7.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Touch Canvas-->
<scene sceneID="984-vW-kks">
<objects>
<viewController id="1fl-Ij-fK1" customClass="ViewController" customModule="TouchCanvas" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="4h2-IA-tif"/>
<viewControllerLayoutGuide type="bottom" id="y0f-7p-uuV"/>
</layoutGuides>
<view key="view" multipleTouchEnabled="YES" contentMode="scaleToFill" id="l3J-kE-Vqx" customClass="CanvasView" customModule="TouchCanvas" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="768" height="1024"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
<navigationItem key="navigationItem" title="Touch Canvas" id="urh-QX-0ux">
<leftBarButtonItems>
<barButtonItem id="89a-qb-NJP">
<button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" id="58x-qE-zXG">
<rect key="frame" x="20" y="7" width="46" height="30"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<state key="normal" title="Debug"/>
<connections>
<action selector="toggleDebugDrawingWithSender:" destination="1fl-Ij-fK1" eventType="touchUpInside" id="BmC-fd-PGt"/>
</connections>
</button>
</barButtonItem>
<barButtonItem id="piu-OG-cVi">
<button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" id="2yf-Ag-uEP">
<rect key="frame" x="74" y="7" width="51" height="30"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<state key="normal" title="Precise"/>
<connections>
<action selector="toggleUsePreciseLocationsWithSender:" destination="1fl-Ij-fK1" eventType="touchUpInside" id="gOd-8G-ZRN"/>
</connections>
</button>
</barButtonItem>
</leftBarButtonItems>
<barButtonItem key="rightBarButtonItem" systemItem="trash" id="Axn-KO-XMW">
<connections>
<action selector="clearViewWithSender:" destination="1fl-Ij-fK1" id="y20-fw-IEo"/>
</connections>
</barButtonItem>
</navigationItem>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="qCw-Nu-BUt" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1647" y="423"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="6oe-Q5-Z99">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="3pu-ol-7ZX" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="mn5-Qr-252">
<rect key="frame" x="0.0" y="20" width="768" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="1fl-Ij-fK1" kind="relationship" relationship="rootViewController" id="2AG-ft-4Zo"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dIr-8x-wol" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="835" y="423"/>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,284 @@
/*
Copyright (C) 2017 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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<UITouch, Line> = 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<UITouch, Line> = 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<UITouch>, 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<UITouch>, 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<NSObject>) {
guard isTouchUpdatingEnabled, let touches = touches as? Set<UITouch> 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)
}
}

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>2.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UIRequiresFullScreen</key>
<true/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,379 @@
/*
Copyright (C) 2017 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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
}
}

View File

@ -0,0 +1,195 @@
/*
Copyright (C) 2017 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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
}
}

View File

@ -0,0 +1,149 @@
/*
Copyright (C) 2017 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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<UITouch>, 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<UITouch>, 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<UITouch>, 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<UITouch>, 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<UITouch>) {
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
}
}
}