From 42c01682ce8af3df4e896fe1db6ef11e06d22b63 Mon Sep 17 00:00:00 2001 From: Brian William Wolter Date: Sun, 14 Aug 2011 15:30:28 -0400 Subject: [PATCH 1/8] Pinned header fixes for TUITableView: - The method -scrollToRowAtIndexPath:atScrollPosition:animated: now accounts for the pinned header when scrolling - Table headers that extend from the new class TUITableViewSectionHeader (which is optional) are notified when they become pinned/unpinned --- lib/UIKit/TUIKit.h | 1 + lib/UIKit/TUITableView.m | 36 ++++++++++++--- lib/UIKit/TUITableViewSectionHeader.h | 36 +++++++++++++++ lib/UIKit/TUITableViewSectionHeader.m | 64 +++++++++++++++++++++++++++ 4 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 lib/UIKit/TUITableViewSectionHeader.h create mode 100644 lib/UIKit/TUITableViewSectionHeader.m diff --git a/lib/UIKit/TUIKit.h b/lib/UIKit/TUIKit.h index 1a6e0f84..3321fcc5 100644 --- a/lib/UIKit/TUIKit.h +++ b/lib/UIKit/TUIKit.h @@ -26,6 +26,7 @@ #import "TUITableView.h" #import "TUITableView+Additions.h" #import "TUITableViewCell.h" +#import "TUITableViewSectionHeader.h" #import "TUILabel.h" #import "TUIImageView.h" #import "TUIButton.h" diff --git a/lib/UIKit/TUITableView.m b/lib/UIKit/TUITableView.m index e7be200e..d55dd9ef 100644 --- a/lib/UIKit/TUITableView.m +++ b/lib/UIKit/TUITableView.m @@ -16,6 +16,7 @@ #import "TUITableView.h" #import "TUITableView+Cell.h" +#import "TUITableViewSectionHeader.h" #import "TUINSView.h" // header views need to be above the cells at all times @@ -682,24 +683,38 @@ - (void)_layoutSectionHeaders:(BOOL)visibleHeadersNeedRelayout TUITableViewSection *section = [_sectionInfo objectAtIndex:index]; if(section.headerView != nil) { CGRect headerFrame = [self rectForHeaderOfSection:index]; - + // check if this header needs to be pinned if(CGRectGetMaxY(headerFrame) > CGRectGetMaxY(visible)) { headerFrame.origin.y = CGRectGetMaxY(visible) - headerFrame.size.height; pinnedHeader = section.headerView; - } - else if((pinnedHeader != nil) && (CGRectGetMaxY(headerFrame) > pinnedHeader.frame.origin.y)) { + // if the header is a TUITableViewSectionHeader notify it of it's pinned state + if([section.headerView isKindOfClass:[TUITableViewSectionHeader class]]){ + ((TUITableViewSectionHeader *)section.headerView).pinnedToViewport = TRUE; + } + }else if((pinnedHeader != nil) && (CGRectGetMaxY(headerFrame) > pinnedHeader.frame.origin.y)) { // this header is intersecting with the pinned header, so we push the pinned header upwards. CGRect pinnedHeaderFrame = pinnedHeader.frame; pinnedHeaderFrame.origin.y = CGRectGetMaxY(headerFrame); pinnedHeader.frame = pinnedHeaderFrame; + // if the header is a TUITableViewSectionHeader notify it of it's pinned state + if([section.headerView isKindOfClass:[TUITableViewSectionHeader class]]){ + ((TUITableViewSectionHeader *)section.headerView).pinnedToViewport = FALSE; + } + }else{ + // if the header is a TUITableViewSectionHeader notify it of it's pinned state + if([section.headerView isKindOfClass:[TUITableViewSectionHeader class]]){ + ((TUITableViewSectionHeader *)section.headerView).pinnedToViewport = FALSE; + } } section.headerView.frame = headerFrame; [section.headerView setNeedsLayout]; - if (section.headerView.superview == nil) + if(section.headerView.superview == nil){ [self addSubview:section.headerView]; + } + } } [_visibleSectionHeaders addIndex:index]; @@ -715,6 +730,7 @@ - (void)_layoutSectionHeaders:(BOOL)visibleHeadersNeedRelayout } [_visibleSectionHeaders removeIndex:index]; }]; + } - (void)_layoutCells:(BOOL)visibleCellsNeedRelayout @@ -924,6 +940,16 @@ - (void)scrollToRowAtIndexPath:(TUIFastIndexPath *)indexPath atScrollPosition:(T { CGRect v = [self visibleRect]; CGRect r = [self rectForRowAtIndexPath:indexPath]; + + // when the target index path section has a header view, add its height to + // the height of our row to prevent the selected row from being overlapped + // by the pinned header + TUIView *headerView; + if((headerView = [self headerViewForSection:indexPath.section]) != nil){ + CGRect headerFrame = [self rectForHeaderOfSection:indexPath.section]; + r.size.height += headerFrame.size.height; + } + switch(scrollPosition) { case TUITableViewScrollPositionNone: // do nothing @@ -937,8 +963,8 @@ - (void)scrollToRowAtIndexPath:(TUIFastIndexPath *)indexPath atScrollPosition:(T default: [self scrollRectToVisible:r animated:animated]; break; - } + } - (TUIFastIndexPath *)indexPathForSelectedRow diff --git a/lib/UIKit/TUITableViewSectionHeader.h b/lib/UIKit/TUITableViewSectionHeader.h new file mode 100644 index 00000000..cd8fb11c --- /dev/null +++ b/lib/UIKit/TUITableViewSectionHeader.h @@ -0,0 +1,36 @@ +/* + Copyright 2011 Twitter, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this work except in compliance with the License. + You may obtain a copy of the License in the LICENSE file, or at: + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import "TUIView.h" + +/** + * @brief An optional base for section header views + * + * A view used as a section header may optionally extend this class, + * in which case the view will recieve messages about header state. + */ +@interface TUITableViewSectionHeader : TUIView { + + BOOL _isPinnedToViewport; + +} + +-(void)headerWillBecomePinned; +-(void)headerWillBecomeUnpinned; + +@property (readwrite, assign, getter=isPinnedToViewport) BOOL pinnedToViewport; + +@end diff --git a/lib/UIKit/TUITableViewSectionHeader.m b/lib/UIKit/TUITableViewSectionHeader.m new file mode 100644 index 00000000..b56e5ab2 --- /dev/null +++ b/lib/UIKit/TUITableViewSectionHeader.m @@ -0,0 +1,64 @@ +/* + Copyright 2011 Twitter, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this work except in compliance with the License. + You may obtain a copy of the License in the LICENSE file, or at: + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import "TUITableViewSectionHeader.h" + +@implementation TUITableViewSectionHeader + +/** + * @brief Determine if this header is currently pinned to the viewport + * + * This method should return TRUE whenever the header is not occupying + * it's normal frame and is overlaping row content. + */ +-(BOOL)isPinnedToViewport { + return _isPinnedToViewport; +} + +/** + * @brief Specify whether this header is currently pinned to the viewport + * @note You should not need to set this property directly, it is managed + * by the table view. + */ +-(void)setPinnedToViewport:(BOOL)pinned { + if(_isPinnedToViewport != pinned){ + if(pinned) [self headerWillBecomePinned]; + else [self headerWillBecomeUnpinned]; + } + _isPinnedToViewport = pinned; +} + +/** + * @brief The header will become pinned + * + * Subclasses may override this method to change the appearance of the header + * when it becomes pinned to the viewport. + */ +-(void)headerWillBecomePinned { + [self setNeedsDisplay]; +} + +/** + * @brief The header will become unpinned + * + * Subclasses may override this method to change the appearance of the header + * when it becomes unpinned from the viewport. + */ +-(void)headerWillBecomeUnpinned { + [self setNeedsDisplay]; +} + +@end From 8dc3abf0155a56c18f3faab90ba0b98301e40b89 Mon Sep 17 00:00:00 2001 From: Brian William Wolter Date: Sun, 14 Aug 2011 15:43:52 -0400 Subject: [PATCH 2/8] Updating example project for table view fixes --- .../ExampleSectionHeaderView.h | 2 +- .../ExampleSectionHeaderView.m | 26 +++++++++++- .../Example.xcodeproj/project.pbxproj | 42 +++++++++++++++++++ lib/UIKit/TUITableView+Cell.m | 9 ++++ 4 files changed, 76 insertions(+), 3 deletions(-) diff --git a/ExampleProject/ConcordeExample/ExampleSectionHeaderView.h b/ExampleProject/ConcordeExample/ExampleSectionHeaderView.h index dc4b3fd0..80da8a0b 100644 --- a/ExampleProject/ConcordeExample/ExampleSectionHeaderView.h +++ b/ExampleProject/ConcordeExample/ExampleSectionHeaderView.h @@ -2,7 +2,7 @@ #import "TUIKit.h" -@interface ExampleSectionHeaderView : TUIView { +@interface ExampleSectionHeaderView : TUITableViewSectionHeader { TUITextRenderer * _labelRenderer; diff --git a/ExampleProject/ConcordeExample/ExampleSectionHeaderView.m b/ExampleProject/ConcordeExample/ExampleSectionHeaderView.m index c46b0f05..c3f14637 100644 --- a/ExampleProject/ConcordeExample/ExampleSectionHeaderView.m +++ b/ExampleProject/ConcordeExample/ExampleSectionHeaderView.m @@ -19,10 +19,27 @@ -(id)initWithFrame:(CGRect)frame { if((self = [super initWithFrame:frame])) { _labelRenderer = [[TUITextRenderer alloc] init]; self.textRenderers = [NSArray arrayWithObjects:_labelRenderer, nil]; + self.opaque = TRUE; } return self; } +/** + * @brief The header will become pinned + */ +-(void)headerWillBecomePinned { + self.opaque = FALSE; + [super headerWillBecomePinned]; +} + +/** + * @brief The header will become unpinned + */ +-(void)headerWillBecomeUnpinned { + self.opaque = TRUE; + [super headerWillBecomeUnpinned]; +} + /** * Drawing */ @@ -32,8 +49,13 @@ -(void)drawRect:(CGRect)rect { if((g = TUIGraphicsGetCurrentContext()) != nil){ [NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithGraphicsPort:g flipped:FALSE]]; - NSColor *start = [NSColor colorWithCalibratedRed:0.8 green:0.8 blue:0.8 alpha:1]; - NSColor *end = [NSColor colorWithCalibratedRed:0.9 green:0.9 blue:0.9 alpha:1]; + if(!self.pinnedToViewport){ + [[NSColor whiteColor] set]; + NSRectFill(self.bounds); + } + + NSColor *start = [NSColor colorWithCalibratedRed:0.8 green:0.8 blue:0.8 alpha:0.9]; + NSColor *end = [NSColor colorWithCalibratedRed:0.9 green:0.9 blue:0.9 alpha:0.9]; NSGradient *gradient = nil; gradient = [[NSGradient alloc] initWithStartingColor:start endingColor:end]; diff --git a/ExampleProject/Example.xcodeproj/project.pbxproj b/ExampleProject/Example.xcodeproj/project.pbxproj index 2d6caa25..4b257789 100644 --- a/ExampleProject/Example.xcodeproj/project.pbxproj +++ b/ExampleProject/Example.xcodeproj/project.pbxproj @@ -68,6 +68,13 @@ 5ED56736139DC35800031CDF /* CoreText+Additions.m in Sources */ = {isa = PBXBuildFile; fileRef = 5ED56732139DC35800031CDF /* CoreText+Additions.m */; }; D3502AAE13EA0FE4007C5CA7 /* TUITableView+Cell.m in Sources */ = {isa = PBXBuildFile; fileRef = D3502AAD13EA0FE4007C5CA7 /* TUITableView+Cell.m */; }; D3CE671313C6646B00D47B2D /* ExampleSectionHeaderView.m in Sources */ = {isa = PBXBuildFile; fileRef = D3CE671213C6646B00D47B2D /* ExampleSectionHeaderView.m */; }; + D3FA4CA913F85B7100860379 /* TUITableViewSectionHeader.m in Sources */ = {isa = PBXBuildFile; fileRef = D3FA4CA813F85B7100860379 /* TUITableViewSectionHeader.m */; }; + D3FA4CBA13F85BC100860379 /* TUIAccessibilityElement.m in Sources */ = {isa = PBXBuildFile; fileRef = D3FA4CAF13F85BC100860379 /* TUIAccessibilityElement.m */; }; + D3FA4CBB13F85BC100860379 /* TUIButton+Accessibility.m in Sources */ = {isa = PBXBuildFile; fileRef = D3FA4CB113F85BC100860379 /* TUIButton+Accessibility.m */; }; + D3FA4CBC13F85BC100860379 /* TUIControl+Accessibility.m in Sources */ = {isa = PBXBuildFile; fileRef = D3FA4CB313F85BC100860379 /* TUIControl+Accessibility.m */; }; + D3FA4CBD13F85BC100860379 /* TUIControl+Private.m in Sources */ = {isa = PBXBuildFile; fileRef = D3FA4CB513F85BC100860379 /* TUIControl+Private.m */; }; + D3FA4CBE13F85BC100860379 /* TUINSView+Accessibility.m in Sources */ = {isa = PBXBuildFile; fileRef = D3FA4CB713F85BC100860379 /* TUINSView+Accessibility.m */; }; + D3FA4CBF13F85BC100860379 /* TUIView+Accessibility.m in Sources */ = {isa = PBXBuildFile; fileRef = D3FA4CB913F85BC100860379 /* TUIView+Accessibility.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -186,6 +193,20 @@ D3502AAD13EA0FE4007C5CA7 /* TUITableView+Cell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "TUITableView+Cell.m"; sourceTree = ""; }; D3CE671113C6646B00D47B2D /* ExampleSectionHeaderView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ExampleSectionHeaderView.h; sourceTree = ""; }; D3CE671213C6646B00D47B2D /* ExampleSectionHeaderView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ExampleSectionHeaderView.m; sourceTree = ""; }; + D3FA4CA713F85B7100860379 /* TUITableViewSectionHeader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TUITableViewSectionHeader.h; sourceTree = ""; }; + D3FA4CA813F85B7100860379 /* TUITableViewSectionHeader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TUITableViewSectionHeader.m; sourceTree = ""; }; + D3FA4CAE13F85BC100860379 /* TUIAccessibilityElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TUIAccessibilityElement.h; sourceTree = ""; }; + D3FA4CAF13F85BC100860379 /* TUIAccessibilityElement.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TUIAccessibilityElement.m; sourceTree = ""; }; + D3FA4CB013F85BC100860379 /* TUIButton+Accessibility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "TUIButton+Accessibility.h"; sourceTree = ""; }; + D3FA4CB113F85BC100860379 /* TUIButton+Accessibility.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "TUIButton+Accessibility.m"; sourceTree = ""; }; + D3FA4CB213F85BC100860379 /* TUIControl+Accessibility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "TUIControl+Accessibility.h"; sourceTree = ""; }; + D3FA4CB313F85BC100860379 /* TUIControl+Accessibility.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "TUIControl+Accessibility.m"; sourceTree = ""; }; + D3FA4CB413F85BC100860379 /* TUIControl+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "TUIControl+Private.h"; sourceTree = ""; }; + D3FA4CB513F85BC100860379 /* TUIControl+Private.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "TUIControl+Private.m"; sourceTree = ""; }; + D3FA4CB613F85BC100860379 /* TUINSView+Accessibility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "TUINSView+Accessibility.h"; sourceTree = ""; }; + D3FA4CB713F85BC100860379 /* TUINSView+Accessibility.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "TUINSView+Accessibility.m"; sourceTree = ""; }; + D3FA4CB813F85BC100860379 /* TUIView+Accessibility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "TUIView+Accessibility.h"; sourceTree = ""; }; + D3FA4CB913F85BC100860379 /* TUIView+Accessibility.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "TUIView+Accessibility.m"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -343,6 +364,8 @@ D3502AAD13EA0FE4007C5CA7 /* TUITableView+Cell.m */, 5ED566D5139DC35100031CDF /* TUITableViewCell.h */, 5ED566D6139DC35100031CDF /* TUITableViewCell.m */, + D3FA4CA713F85B7100860379 /* TUITableViewSectionHeader.h */, + D3FA4CA813F85B7100860379 /* TUITableViewSectionHeader.m */, 5ED56698139DC35100031CDF /* TUIActivityIndicatorView.h */, 5ED56699139DC35100031CDF /* TUIActivityIndicatorView.m */, 5ED56696139DC35100031CDF /* TUIAccessibility.h */, @@ -374,6 +397,18 @@ 5ED566A0139DC35100031CDF /* TUIButton+Content.m */, 5ED566F5139DC35100031CDF /* TUIViewNSViewContainer.h */, 5C55D83013A663E2000ED768 /* TUIViewNSViewContainer.m */, + D3FA4CAE13F85BC100860379 /* TUIAccessibilityElement.h */, + D3FA4CAF13F85BC100860379 /* TUIAccessibilityElement.m */, + D3FA4CB013F85BC100860379 /* TUIButton+Accessibility.h */, + D3FA4CB113F85BC100860379 /* TUIButton+Accessibility.m */, + D3FA4CB213F85BC100860379 /* TUIControl+Accessibility.h */, + D3FA4CB313F85BC100860379 /* TUIControl+Accessibility.m */, + D3FA4CB413F85BC100860379 /* TUIControl+Private.h */, + D3FA4CB513F85BC100860379 /* TUIControl+Private.m */, + D3FA4CB613F85BC100860379 /* TUINSView+Accessibility.h */, + D3FA4CB713F85BC100860379 /* TUINSView+Accessibility.m */, + D3FA4CB813F85BC100860379 /* TUIView+Accessibility.h */, + D3FA4CB913F85BC100860379 /* TUIView+Accessibility.m */, 5C90DB9A13A6D52000ECDD14 /* Extras */, ); name = TUIKit; @@ -495,6 +530,13 @@ D3CE671313C6646B00D47B2D /* ExampleSectionHeaderView.m in Sources */, D3502AAE13EA0FE4007C5CA7 /* TUITableView+Cell.m in Sources */, 5ED06F2C13F4C98800B34CAE /* TUITextViewEditor.m in Sources */, + D3FA4CA913F85B7100860379 /* TUITableViewSectionHeader.m in Sources */, + D3FA4CBA13F85BC100860379 /* TUIAccessibilityElement.m in Sources */, + D3FA4CBB13F85BC100860379 /* TUIButton+Accessibility.m in Sources */, + D3FA4CBC13F85BC100860379 /* TUIControl+Accessibility.m in Sources */, + D3FA4CBD13F85BC100860379 /* TUIControl+Private.m in Sources */, + D3FA4CBE13F85BC100860379 /* TUINSView+Accessibility.m in Sources */, + D3FA4CBF13F85BC100860379 /* TUIView+Accessibility.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/lib/UIKit/TUITableView+Cell.m b/lib/UIKit/TUITableView+Cell.m index bb2e7a19..03019119 100644 --- a/lib/UIKit/TUITableView+Cell.m +++ b/lib/UIKit/TUITableView+Cell.m @@ -16,6 +16,9 @@ #import "TUITableView+Cell.h" +// Dragged cells should be just above pinned headers +#define kTUITableViewDraggedCellZPosition 1001 + @interface TUITableView (CellPrivate) - (BOOL)_preLayoutCells; @@ -96,6 +99,9 @@ -(void)__updateDraggingCell:(TUITableViewCell *)cell offset:(CGPoint)offset loca // initialize defaults on the first drag if(_currentDragToReorderIndexPath == nil || _previousDragToReorderIndexPath == nil){ + // make sure the dragged cell is on top + _dragToReorderCell.layer.zPosition = kTUITableViewDraggedCellZPosition; + // setup index paths [_currentDragToReorderIndexPath release]; _currentDragToReorderIndexPath = [cell.indexPath retain]; [_previousDragToReorderIndexPath release]; @@ -348,6 +354,9 @@ -(void)__endDraggingCell:(TUITableViewCell *)cell offset:(CGPoint)offset locatio [_previousDragToReorderIndexPath release]; _previousDragToReorderIndexPath = nil; + // restore the dragged cell z-position + _dragToReorderCell.layer.zPosition = 0; + // and clean up [_dragToReorderCell release]; _dragToReorderCell = nil; From 69197b01d676fc8980d6af086986ff079b73e9a9 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 15 Aug 2011 14:04:23 -0400 Subject: [PATCH 3/8] gotta actually add TUITableViewSectionHeader to the project --- TwUI.xcodeproj/project.pbxproj | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/TwUI.xcodeproj/project.pbxproj b/TwUI.xcodeproj/project.pbxproj index 44c81c24..530df537 100644 --- a/TwUI.xcodeproj/project.pbxproj +++ b/TwUI.xcodeproj/project.pbxproj @@ -78,6 +78,12 @@ 886EBA8313D64393006DE018 /* TUIControl+Private.m in Sources */ = {isa = PBXBuildFile; fileRef = 886EBA7E13D64393006DE018 /* TUIControl+Private.m */; }; 886EBA8413D64393006DE018 /* TUIControl+Private.m in Sources */ = {isa = PBXBuildFile; fileRef = 886EBA7E13D64393006DE018 /* TUIControl+Private.m */; }; 886EBA8513D64393006DE018 /* TUIControl+Private.m in Sources */ = {isa = PBXBuildFile; fileRef = 886EBA7E13D64393006DE018 /* TUIControl+Private.m */; }; + 887F272C13F9969800D75DE6 /* TUITableViewSectionHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = 887F272A13F9969800D75DE6 /* TUITableViewSectionHeader.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 887F272D13F9969800D75DE6 /* TUITableViewSectionHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = 887F272A13F9969800D75DE6 /* TUITableViewSectionHeader.h */; }; + 887F272E13F9969800D75DE6 /* TUITableViewSectionHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = 887F272A13F9969800D75DE6 /* TUITableViewSectionHeader.h */; }; + 887F272F13F9969800D75DE6 /* TUITableViewSectionHeader.m in Sources */ = {isa = PBXBuildFile; fileRef = 887F272B13F9969800D75DE6 /* TUITableViewSectionHeader.m */; }; + 887F273013F9969800D75DE6 /* TUITableViewSectionHeader.m in Sources */ = {isa = PBXBuildFile; fileRef = 887F272B13F9969800D75DE6 /* TUITableViewSectionHeader.m */; }; + 887F273113F9969800D75DE6 /* TUITableViewSectionHeader.m in Sources */ = {isa = PBXBuildFile; fileRef = 887F272B13F9969800D75DE6 /* TUITableViewSectionHeader.m */; }; 88CC1F2F13E365B600827793 /* TUIControl+Accessibility.h in Headers */ = {isa = PBXBuildFile; fileRef = 88CC1F2D13E365B500827793 /* TUIControl+Accessibility.h */; }; 88CC1F3013E365B600827793 /* TUIControl+Accessibility.h in Headers */ = {isa = PBXBuildFile; fileRef = 88CC1F2D13E365B500827793 /* TUIControl+Accessibility.h */; }; 88CC1F3113E365B600827793 /* TUIControl+Accessibility.h in Headers */ = {isa = PBXBuildFile; fileRef = 88CC1F2D13E365B500827793 /* TUIControl+Accessibility.h */; }; @@ -266,6 +272,8 @@ 8819794B13E26E5800AA39EB /* TUINSView+Accessibility.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "TUINSView+Accessibility.m"; sourceTree = ""; }; 886EBA7D13D64393006DE018 /* TUIControl+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "TUIControl+Private.h"; sourceTree = ""; }; 886EBA7E13D64393006DE018 /* TUIControl+Private.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "TUIControl+Private.m"; sourceTree = ""; }; + 887F272A13F9969800D75DE6 /* TUITableViewSectionHeader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TUITableViewSectionHeader.h; sourceTree = ""; }; + 887F272B13F9969800D75DE6 /* TUITableViewSectionHeader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TUITableViewSectionHeader.m; sourceTree = ""; }; 88CC1F2D13E365B500827793 /* TUIControl+Accessibility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "TUIControl+Accessibility.h"; sourceTree = ""; }; 88CC1F2E13E365B500827793 /* TUIControl+Accessibility.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "TUIControl+Accessibility.m"; sourceTree = ""; }; 88CC1F3513E3684400827793 /* TUIButton+Accessibility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "TUIButton+Accessibility.h"; sourceTree = ""; }; @@ -593,6 +601,8 @@ CBB74C7413BE6E1900C85CB5 /* TUITableViewCell.m */, 88D25F5313F5D96500CFAAA9 /* TUITableView+Cell.h */, 88D25F5413F5D96500CFAAA9 /* TUITableView+Cell.m */, + 887F272A13F9969800D75DE6 /* TUITableViewSectionHeader.h */, + 887F272B13F9969800D75DE6 /* TUITableViewSectionHeader.m */, CBB74C7513BE6E1900C85CB5 /* TUITextEditor.h */, CBB74C7613BE6E1900C85CB5 /* TUITextEditor.m */, CBB74C7713BE6E1900C85CB5 /* TUITextField.h */, @@ -644,6 +654,7 @@ 88CC1F3913E3684700827793 /* TUIButton+Accessibility.h in Headers */, 88EFFB5313F417E200CF91A9 /* TUITextViewEditor.h in Headers */, 88D25F5713F5D96500CFAAA9 /* TUITableView+Cell.h in Headers */, + 887F272E13F9969800D75DE6 /* TUITableViewSectionHeader.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -692,6 +703,7 @@ CBB74CE213BE6E1900C85CB5 /* TUIView.h in Headers */, CBB74CE413BE6E1900C85CB5 /* TUIViewController.h in Headers */, CBB74CE613BE6E1900C85CB5 /* TUIViewNSViewContainer.h in Headers */, + 887F272C13F9969800D75DE6 /* TUITableViewSectionHeader.h in Headers */, 886EBA7F13D64393006DE018 /* TUIControl+Private.h in Headers */, 8819794413E26E0200AA39EB /* TUIView+Accessibility.h in Headers */, 8819794C13E26E5800AA39EB /* TUINSView+Accessibility.h in Headers */, @@ -713,6 +725,7 @@ 88CC1F3813E3684700827793 /* TUIButton+Accessibility.h in Headers */, 88EFFB5213F417E200CF91A9 /* TUITextViewEditor.h in Headers */, 88D25F5613F5D96500CFAAA9 /* TUITableView+Cell.h in Headers */, + 887F272D13F9969800D75DE6 /* TUITableViewSectionHeader.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -912,6 +925,7 @@ 88CC1F3C13E3684700827793 /* TUIButton+Accessibility.m in Sources */, 88EFFB5613F417E200CF91A9 /* TUITextViewEditor.m in Sources */, 88D25F5A13F5D96500CFAAA9 /* TUITableView+Cell.m in Sources */, + 887F273113F9969800D75DE6 /* TUITableViewSectionHeader.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -972,6 +986,7 @@ 88CC1F3A13E3684700827793 /* TUIButton+Accessibility.m in Sources */, 88EFFB5413F417E200CF91A9 /* TUITextViewEditor.m in Sources */, 88D25F5813F5D96500CFAAA9 /* TUITableView+Cell.m in Sources */, + 887F272F13F9969800D75DE6 /* TUITableViewSectionHeader.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1041,6 +1056,7 @@ 88CC1F3B13E3684700827793 /* TUIButton+Accessibility.m in Sources */, 88EFFB5513F417E200CF91A9 /* TUITextViewEditor.m in Sources */, 88D25F5913F5D96500CFAAA9 /* TUITableView+Cell.m in Sources */, + 887F273013F9969800D75DE6 /* TUITableViewSectionHeader.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 3dbc67a1305236989d634b559ad8df3383e7a9a5 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 15 Aug 2011 13:58:46 -0400 Subject: [PATCH 4/8] moved the control event sending into TUIControl --- lib/UIKit/TUIButton.m | 21 +-------------------- lib/UIKit/TUIControl.m | 29 ++++++++++++++++------------- 2 files changed, 17 insertions(+), 33 deletions(-) diff --git a/lib/UIKit/TUIButton.m b/lib/UIKit/TUIButton.m index 7cc17788..6360c2eb 100644 --- a/lib/UIKit/TUIButton.m +++ b/lib/UIKit/TUIButton.m @@ -215,12 +215,7 @@ - (void)drawRect:(CGRect)r - (void)mouseDown:(NSEvent *)event { [super mouseDown:event]; - if([event clickCount] < 2) { - [self sendActionsForControlEvents:TUIControlEventTouchDown]; - } else { - [self sendActionsForControlEvents:TUIControlEventTouchDownRepeat]; - } - + if(popUpMenu) { // happens even if clickCount is big NSMenu *menu = popUpMenu; NSPoint p = [self frameInNSView].origin; @@ -240,20 +235,6 @@ - (void)mouseDown:(NSEvent *)event } } -- (void)mouseUp:(NSEvent *)event -{ - [super mouseUp:event]; -// if([event clickCount] < 2) { - if([self eventInside:event]) { - if(![self didDrag]) { - [self sendActionsForControlEvents:TUIControlEventTouchUpInside]; - } - } else { - [self sendActionsForControlEvents:TUIControlEventTouchUpOutside]; - } -// } -} - - (void)_update { _titleView.text = self.currentTitle; _titleView.textColor = self.currentTitleColor; diff --git a/lib/UIKit/TUIControl.m b/lib/UIKit/TUIControl.m index 9b9d3197..9be222ff 100644 --- a/lib/UIKit/TUIControl.m +++ b/lib/UIKit/TUIControl.m @@ -95,9 +95,9 @@ -(BOOL)selected { */ -(void)setSelected:(BOOL)selected { [self _stateWillChange]; - _controlFlags.selected = selected; + _controlFlags.selected = selected; [self _stateDidChange]; - [self setNeedsDisplay]; + [self setNeedsDisplay]; } - (BOOL)acceptsFirstMouse @@ -124,12 +124,15 @@ - (void)mouseDown:(NSEvent *)event _controlFlags.tracking = 1; [self _stateDidChange]; - // handle touch down - [self sendActionsForControlEvents:TUIControlEventTouchDown]; + // handle touch down + if([event clickCount] < 2) { + [self sendActionsForControlEvents:TUIControlEventTouchDown]; + } else { + [self sendActionsForControlEvents:TUIControlEventTouchDownRepeat]; + } // needs display [self setNeedsDisplay]; - } - (void)mouseUp:(NSEvent *)event @@ -141,16 +144,16 @@ - (void)mouseUp:(NSEvent *)event _controlFlags.tracking = 0; [self _stateDidChange]; - // handle touch up - if([self pointInside:[self localPointForEvent:event] withEvent:event]){ - [self sendActionsForControlEvents:TUIControlEventTouchUpInside]; - }else{ - [self sendActionsForControlEvents:TUIControlEventTouchUpOutside]; - } + if([self eventInside:event]) { + if(![self didDrag]) { + [self sendActionsForControlEvents:TUIControlEventTouchUpInside]; + } + } else { + [self sendActionsForControlEvents:TUIControlEventTouchUpOutside]; + } - // needs display + // needs display [self setNeedsDisplay]; - } @end From 78f47fd5351c2bb8b3d4a777b2f160375266c999 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 15 Aug 2011 13:59:26 -0400 Subject: [PATCH 5/8] some work on spell checking in tuitextview --- lib/UIKit/TUITextView.h | 4 ++++ lib/UIKit/TUITextView.m | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/lib/UIKit/TUITextView.h b/lib/UIKit/TUITextView.h index be2c8bb8..0b07e37b 100644 --- a/lib/UIKit/TUITextView.h +++ b/lib/UIKit/TUITextView.h @@ -35,6 +35,9 @@ TUIColor *textColor; TUITextAlignment textAlignment; BOOL editable; + + BOOL spellCheckingEnabled; + NSInteger lastSpellCheckToken; TUIEdgeInsets contentInset; @@ -62,6 +65,7 @@ @property (nonatomic, assign) NSRange selectedRange; @property (nonatomic, assign, getter=isEditable) BOOL editable; +@property (nonatomic, assign, getter=isSpellCheckingEnabled) BOOL spellCheckingEnabled; @property (nonatomic, copy) TUIViewDrawRect drawFrame; diff --git a/lib/UIKit/TUITextView.m b/lib/UIKit/TUITextView.m index 4c1e6163..46e8c073 100644 --- a/lib/UIKit/TUITextView.m +++ b/lib/UIKit/TUITextView.m @@ -18,6 +18,10 @@ #import "TUITextView.h" #import "TUITextViewEditor.h" +@interface TUITextView () +- (void)checkSpelling; +@end + @implementation TUITextView @synthesize delegate; @@ -28,6 +32,7 @@ @implementation TUITextView @synthesize editable; @synthesize contentInset; @synthesize placeholder; +@synthesize spellCheckingEnabled; - (void)_updateDefaultAttributes { @@ -259,6 +264,34 @@ - (void)_textDidChange { if(_textViewFlags.delegateTextViewDidChange) [delegate textViewDidChange:self]; + + // We only want to spell-check once they're done typing because it's super annoying to see the red wrong-spelling underline as you're typing out a word. So delay the spell check until we don't get any more text changes. + if(spellCheckingEnabled) { + static const NSTimeInterval spellCheckDelay = 0.3f; + [[self class] cancelPreviousPerformRequestsWithTarget:self selector:@selector(checkSpelling) object:nil]; + [self performSelector:@selector(checkSpelling) withObject:nil afterDelay:spellCheckDelay]; + } +} + +- (void)checkSpelling +{ + lastSpellCheckToken = [[NSSpellChecker sharedSpellChecker] requestCheckingOfString:self.text range:NSMakeRange(0, [self.text length]) types:NSTextCheckingTypeSpelling options:nil inSpellDocumentWithTag:0 completionHandler:^(NSInteger sequenceNumber, NSArray *results, NSOrthography *orthography, NSInteger wordCount) { + // This needs to happen on the main thread so that the user doesn't enter more text while we're changing the attributed string. + dispatch_async(dispatch_get_main_queue(), ^{ + // we only care about the most recent results, ignore anything older + if(sequenceNumber != lastSpellCheckToken) return; + + [[renderer backingStore] removeAttribute:(id)kCTUnderlineColorAttributeName range:NSMakeRange(0, [self.text length])]; + [[renderer backingStore] removeAttribute:(id)kCTUnderlineStyleAttributeName range:NSMakeRange(0, [self.text length])]; + + for(NSTextCheckingResult *result in results) { + [[renderer backingStore] addAttribute:(id)kCTUnderlineColorAttributeName value:(id)[TUIColor redColor].CGColor range:result.range]; + [[renderer backingStore] addAttribute:(id)kCTUnderlineStyleAttributeName value:[NSNumber numberWithInteger:kCTUnderlineStyleThick | kCTUnderlinePatternDot] range:result.range]; + } + + [self setNeedsDisplay]; + }); + }]; } - (NSRange)selectedRange From 65c40b9b07f275ae27184f6f1650625dbcc00ae1 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 15 Aug 2011 17:13:11 -0400 Subject: [PATCH 6/8] expose -stringIndexForEvent: --- lib/UIKit/TUITextRenderer+Event.h | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/UIKit/TUITextRenderer+Event.h b/lib/UIKit/TUITextRenderer+Event.h index abfcf28a..cb04984a 100644 --- a/lib/UIKit/TUITextRenderer+Event.h +++ b/lib/UIKit/TUITextRenderer+Event.h @@ -19,6 +19,7 @@ @interface TUITextRenderer (Event) - (CFIndex)stringIndexForPoint:(CGPoint)p; +- (CFIndex)stringIndexForEvent:(NSEvent *)event; - (void)resetSelection; - (CGRect)rectForCurrentSelection; From 789e4bcbee69e7793e089d67c8a677b89025a6a2 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 15 Aug 2011 17:16:30 -0400 Subject: [PATCH 7/8] show the replacement contextual menu and actually do the replacement --- lib/UIKit/TUITextView.h | 6 ++- lib/UIKit/TUITextView.m | 88 +++++++++++++++++++++++++++++++++++------ 2 files changed, 82 insertions(+), 12 deletions(-) diff --git a/lib/UIKit/TUITextView.h b/lib/UIKit/TUITextView.h index 0b07e37b..4221a399 100644 --- a/lib/UIKit/TUITextView.h +++ b/lib/UIKit/TUITextView.h @@ -37,7 +37,10 @@ BOOL editable; BOOL spellCheckingEnabled; - NSInteger lastSpellCheckToken; + NSInteger lastCheckToken; + NSArray *lastCheckResults; + NSTextCheckingResult *selectedTextCheckingResult; + BOOL autocorrectionEnabled; TUIEdgeInsets contentInset; @@ -66,6 +69,7 @@ @property (nonatomic, assign) NSRange selectedRange; @property (nonatomic, assign, getter=isEditable) BOOL editable; @property (nonatomic, assign, getter=isSpellCheckingEnabled) BOOL spellCheckingEnabled; +@property (nonatomic, assign, getter=isAutocorrectionEnabled) BOOL autocorrectionEnabled; @property (nonatomic, copy) TUIViewDrawRect drawFrame; diff --git a/lib/UIKit/TUITextView.m b/lib/UIKit/TUITextView.m index 46e8c073..512b5038 100644 --- a/lib/UIKit/TUITextView.m +++ b/lib/UIKit/TUITextView.m @@ -17,9 +17,14 @@ #import "TUIKit.h" #import "TUITextView.h" #import "TUITextViewEditor.h" +#import "TUITextRenderer+Event.h" @interface TUITextView () -- (void)checkSpelling; +- (void)_checkSpelling; +- (void)_replaceMisspelledWord:(NSMenuItem *)menuItem; + +@property (nonatomic, retain) NSArray *lastCheckResults; +@property (nonatomic, strong) NSTextCheckingResult *selectedTextCheckingResult; @end @implementation TUITextView @@ -33,6 +38,9 @@ @implementation TUITextView @synthesize contentInset; @synthesize placeholder; @synthesize spellCheckingEnabled; +@synthesize lastCheckResults; +@synthesize selectedTextCheckingResult; +@synthesize autocorrectionEnabled; - (void)_updateDefaultAttributes { @@ -81,6 +89,8 @@ - (void)dealloc [font release]; [textColor release]; [placeholder release]; + [lastCheckResults release]; + [selectedTextCheckingResult release]; [super dealloc]; } @@ -265,35 +275,91 @@ - (void)_textDidChange if(_textViewFlags.delegateTextViewDidChange) [delegate textViewDidChange:self]; - // We only want to spell-check once they're done typing because it's super annoying to see the red wrong-spelling underline as you're typing out a word. So delay the spell check until we don't get any more text changes. if(spellCheckingEnabled) { - static const NSTimeInterval spellCheckDelay = 0.3f; - [[self class] cancelPreviousPerformRequestsWithTarget:self selector:@selector(checkSpelling) object:nil]; - [self performSelector:@selector(checkSpelling) withObject:nil afterDelay:spellCheckDelay]; + [self _checkSpelling]; } } -- (void)checkSpelling +- (void)_checkSpelling { - lastSpellCheckToken = [[NSSpellChecker sharedSpellChecker] requestCheckingOfString:self.text range:NSMakeRange(0, [self.text length]) types:NSTextCheckingTypeSpelling options:nil inSpellDocumentWithTag:0 completionHandler:^(NSInteger sequenceNumber, NSArray *results, NSOrthography *orthography, NSInteger wordCount) { + lastCheckToken = [[NSSpellChecker sharedSpellChecker] requestCheckingOfString:self.text range:NSMakeRange(0, [self.text length]) types:NSTextCheckingTypeSpelling options:nil inSpellDocumentWithTag:0 completionHandler:^(NSInteger sequenceNumber, NSArray *results, NSOrthography *orthography, NSInteger wordCount) { // This needs to happen on the main thread so that the user doesn't enter more text while we're changing the attributed string. dispatch_async(dispatch_get_main_queue(), ^{ // we only care about the most recent results, ignore anything older - if(sequenceNumber != lastSpellCheckToken) return; + if(sequenceNumber != lastCheckToken) return; + + [[renderer backingStore] beginEditing]; - [[renderer backingStore] removeAttribute:(id)kCTUnderlineColorAttributeName range:NSMakeRange(0, [self.text length])]; - [[renderer backingStore] removeAttribute:(id)kCTUnderlineStyleAttributeName range:NSMakeRange(0, [self.text length])]; - + NSRange wholeStringRange = NSMakeRange(0, [self.text length]); + [[renderer backingStore] removeAttribute:(id)kCTUnderlineColorAttributeName range:wholeStringRange]; + [[renderer backingStore] removeAttribute:(id)kCTUnderlineStyleAttributeName range:wholeStringRange]; + + NSRange selectionRange = [self selectedRange]; for(NSTextCheckingResult *result in results) { + // Don't spell check the word they're typing, otherwise we're constantly marking it as misspelled and that's lame. + BOOL isActiveWord = (result.range.location + result.range.length == selectionRange.location) && selectionRange.length == 0; + if(isActiveWord) continue; + [[renderer backingStore] addAttribute:(id)kCTUnderlineColorAttributeName value:(id)[TUIColor redColor].CGColor range:result.range]; [[renderer backingStore] addAttribute:(id)kCTUnderlineStyleAttributeName value:[NSNumber numberWithInteger:kCTUnderlineStyleThick | kCTUnderlinePatternDot] range:result.range]; } + + [[renderer backingStore] endEditing]; + [renderer reset]; // make sure we reset so that the renderer uses our new attributes [self setNeedsDisplay]; + + self.lastCheckResults = results; }); }]; } +- (NSMenu *)menuForEvent:(NSEvent *)event +{ + CFIndex stringIndex = [renderer stringIndexForEvent:event]; + for(NSTextCheckingResult *result in lastCheckResults) { + if(stringIndex >= result.range.location && stringIndex <= result.range.location + result.range.length) { + self.selectedTextCheckingResult = result; + break; + } + } + + if(selectedTextCheckingResult == nil) return nil; + + NSMenu *menu = [[NSMenu alloc] initWithTitle:@""]; + NSArray *guesses = [[NSSpellChecker sharedSpellChecker] guessesForWordRange:selectedTextCheckingResult.range inString:[self text] language:nil inSpellDocumentWithTag:0]; + if(guesses.count > 0) { + for(NSString *guess in guesses) { + NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle:guess action:@selector(_replaceMisspelledWord:) keyEquivalent:@""]; + [menuItem setTarget:self]; + [menuItem setRepresentedObject:guess]; + [menu addItem:menuItem]; + [menuItem release]; + } + } else { + NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle:@"No guesses" action:NULL keyEquivalent:@""]; + [menu addItem:menuItem]; + [menuItem release]; + } + + return [menu autorelease]; +} + +- (void)_replaceMisspelledWord:(NSMenuItem *)menuItem +{ + NSString *replacement = [menuItem representedObject]; + [[renderer backingStore] beginEditing]; + [[renderer backingStore] removeAttribute:(id)kCTUnderlineColorAttributeName range:selectedTextCheckingResult.range]; + [[renderer backingStore] removeAttribute:(id)kCTUnderlineStyleAttributeName range:selectedTextCheckingResult.range]; + [[renderer backingStore] replaceCharactersInRange:self.selectedTextCheckingResult.range withString:replacement]; + [[renderer backingStore] endEditing]; + [renderer reset]; + + [self _textDidChange]; + + self.selectedTextCheckingResult = nil; +} + - (NSRange)selectedRange { return [renderer selectedRange]; From 4a7e71618ba218ff182b6b836615a7d8f499792a Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 15 Aug 2011 17:22:25 -0400 Subject: [PATCH 8/8] s/strong/retain for people not on Xcode 4.2 --- lib/UIKit/TUITextView.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/UIKit/TUITextView.m b/lib/UIKit/TUITextView.m index 512b5038..826426a4 100644 --- a/lib/UIKit/TUITextView.m +++ b/lib/UIKit/TUITextView.m @@ -24,7 +24,7 @@ - (void)_checkSpelling; - (void)_replaceMisspelledWord:(NSMenuItem *)menuItem; @property (nonatomic, retain) NSArray *lastCheckResults; -@property (nonatomic, strong) NSTextCheckingResult *selectedTextCheckingResult; +@property (nonatomic, retain) NSTextCheckingResult *selectedTextCheckingResult; @end @implementation TUITextView