├── .gitignore ├── Cartfile ├── Cartfile.resolved ├── Crashlytics.framework ├── Crashlytics ├── Headers │ ├── ANSCompatibility.h │ ├── Answers.h │ ├── CLSAttributes.h │ ├── CLSLogging.h │ ├── CLSReport.h │ ├── CLSStackFrame.h │ └── Crashlytics.h ├── Info.plist ├── Modules │ └── module.modulemap ├── run ├── submit └── uploadDSYM ├── Fabric.framework ├── Fabric ├── Headers │ ├── FABAttributes.h │ └── Fabric.h ├── Info.plist ├── Modules │ └── module.modulemap ├── run └── uploadDSYM ├── LICENSE ├── README.md ├── ScreenShot ├── s_1.jpg └── s_2.jpg ├── v2ex.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcbaselines │ └── 97BA7CEC1AF47FBB00950A0C.xcbaseline │ ├── D1287C0B-8F0F-41B0-83DA-12EE4B5C41B0.plist │ └── Info.plist ├── v2ex ├── APIManage.swift ├── AccountViewController.swift ├── AppDelegate.swift ├── AtUserTableView.swift ├── Base.lproj │ ├── LaunchScreen.xib │ └── Main.storyboard ├── BaseViewController.swift ├── CommentCell.swift ├── CommentCell.xib ├── CommentModel.swift ├── HotViewController.swift ├── Images.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon29@2x.png │ │ ├── icon29@3x.png │ │ ├── icon40@2x.png │ │ ├── icon40@3x.png │ │ ├── icon60@2x.png │ │ ├── icon60@3x.png │ │ ├── icon76.png │ │ ├── icon76@2x.png │ │ └── icon83.5@2x.png │ ├── first.imageset │ │ ├── Contents.json │ │ └── first.pdf │ ├── homeTabbar.imageset │ │ ├── Contents.json │ │ ├── homeTabbar@2x.png │ │ └── homeTabbar@3x.png │ ├── hotTabbar.imageset │ │ ├── Contents.json │ │ ├── hot@2x.png │ │ └── hot@3x.png │ ├── latestTabbar.imageset │ │ ├── Contents.json │ │ ├── latest@2x.png │ │ └── latest@3x.png │ ├── nodeTabbar.imageset │ │ ├── Contents.json │ │ ├── nodeTabbar@2x.png │ │ └── nodeTabbar@3x.png │ ├── second.imageset │ │ ├── Contents.json │ │ └── second.pdf │ ├── webBack.imageset │ │ ├── Contents.json │ │ └── webBack@2x.png │ ├── webForward.imageset │ │ ├── Contents.json │ │ └── webForward@2x.png │ ├── webMore.imageset │ │ ├── Contents.json │ │ └── webMore@2x.png │ ├── webRefresh.imageset │ │ ├── Contents.json │ │ └── webRefresh@2x.png │ └── webStop.imageset │ │ ├── Contents.json │ │ └── webStop@2x.png ├── Info.plist ├── JSONAble.swift ├── LatestViewController.swift ├── MemberCell.swift ├── MemberCell.xib ├── MemberModel.swift ├── MemberReplyCell.swift ├── MemberReplyModel.swift ├── MemberReplyViewController.swift ├── NodeModel.swift ├── NodeViewController.swift ├── NotificationCell.swift ├── NotificationManage.swift ├── NotificationModel.swift ├── NotificationViewController.swift ├── PostCell.swift ├── PostCell.xib ├── PostContentCell.swift ├── PostDetailModel.swift ├── PostDetailViewController.swift ├── PostModel.swift ├── PostTableView.swift ├── PostViewController.swift ├── ProfileViewController.swift ├── Vendor │ ├── DAKeyboardControl │ │ ├── DAKeyboardControl.h │ │ └── DAKeyboardControl.m │ ├── FlurrySDK │ │ └── Flurry │ │ │ ├── Empty.m │ │ │ ├── Flurry.h │ │ │ ├── FlurryWatch.h │ │ │ └── libFlurry_7.3.0.a │ ├── JDStatusBarNotification │ │ ├── JDStatusBarNotification.h │ │ ├── JDStatusBarNotification.m │ │ ├── JDStatusBarStyle.h │ │ ├── JDStatusBarStyle.m │ │ ├── JDStatusBarView.h │ │ └── JDStatusBarView.m │ └── TDBadgedCell │ │ ├── TDBadgedCell.h │ │ └── TDBadgedCell.m ├── WebViewController.swift ├── v2ex-Bridging-Header.h ├── v2ex.entitlements └── v2ex.playground │ ├── Contents.swift │ ├── Sources │ └── SupportCode.swift │ ├── contents.xcplayground │ └── timeline.xctimeline ├── v2exKit ├── Info.plist ├── STColor.swift ├── STDate.swift ├── STInsetLabel.swift ├── STString.swift ├── STTextView.swift ├── STView.swift ├── v2exKit.h └── v2exKit.swift ├── v2exKitTests ├── Info.plist └── v2exKitTests.swift ├── v2exTests ├── Info.plist └── v2exTests.swift └── v2exTodayExtension ├── Info.plist ├── MainInterface.storyboard ├── TodayViewController.swift └── v2exTodayExtension.entitlements /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | *.xccheckout 14 | *.moved-aside 15 | DerivedData 16 | *.hmap 17 | *.ipa 18 | *.xcuserstate 19 | 20 | # CocoaPods 21 | # 22 | # We recommend against adding the Pods directory to your .gitignore. However 23 | # you should judge for yourself, the pros and cons are mentioned at: 24 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 25 | # 26 | Pods/ 27 | 28 | # Carthage 29 | # 30 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 31 | Carthage/Checkouts 32 | 33 | Carthage/Build 34 | 35 | *.psd 36 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "tid-kijyun/Kanna" ~> 1.0.0 2 | github "onevcat/Kingfisher" ~> 1.9 3 | github "SnapKit/SnapKit" 4 | github "radex/SwiftyUserDefaults" 5 | github "TTTAttributedLabel/TTTAttributedLabel" 6 | github "thii/FontAwesome.swift" "0.7.0" 7 | github "SwiftyJSON/SwiftyJSON" 8 | github "Alamofire/Alamofire" ~> 3.0 -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "Alamofire/Alamofire" "3.3.0" 2 | github "thii/FontAwesome.swift" "0.7.0" 3 | github "tid-kijyun/Kanna" "1.0.6" 4 | github "onevcat/Kingfisher" "1.9.3" 5 | github "SnapKit/SnapKit" "0.19.1" 6 | github "SwiftyJSON/SwiftyJSON" "2.3.3" 7 | github "radex/SwiftyUserDefaults" "2.1.3" 8 | github "TTTAttributedLabel/TTTAttributedLabel" "1.13.4" 9 | -------------------------------------------------------------------------------- /Crashlytics.framework/Crashlytics: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/Crashlytics.framework/Crashlytics -------------------------------------------------------------------------------- /Crashlytics.framework/Headers/ANSCompatibility.h: -------------------------------------------------------------------------------- 1 | // 2 | // ANSCompatibility.h 3 | // AnswersKit 4 | // 5 | // Copyright (c) 2015 Crashlytics, Inc. All rights reserved. 6 | // 7 | 8 | #pragma once 9 | 10 | #if !__has_feature(nullability) 11 | #define nonnull 12 | #define nullable 13 | #define _Nullable 14 | #define _Nonnull 15 | #endif 16 | 17 | #ifndef NS_ASSUME_NONNULL_BEGIN 18 | #define NS_ASSUME_NONNULL_BEGIN 19 | #endif 20 | 21 | #ifndef NS_ASSUME_NONNULL_END 22 | #define NS_ASSUME_NONNULL_END 23 | #endif 24 | 25 | #if __has_feature(objc_generics) 26 | #define ANS_GENERIC_NSARRAY(type) NSArray 27 | #define ANS_GENERIC_NSDICTIONARY(key_type,object_key) NSDictionary 28 | #else 29 | #define ANS_GENERIC_NSARRAY(type) NSArray 30 | #define ANS_GENERIC_NSDICTIONARY(key_type,object_key) NSDictionary 31 | #endif 32 | -------------------------------------------------------------------------------- /Crashlytics.framework/Headers/CLSAttributes.h: -------------------------------------------------------------------------------- 1 | // 2 | // CLSAttributes.h 3 | // Crashlytics 4 | // 5 | // Copyright (c) 2015 Crashlytics, Inc. All rights reserved. 6 | // 7 | 8 | #pragma once 9 | 10 | #define CLS_DEPRECATED(x) __attribute__ ((deprecated(x))) 11 | 12 | #if !__has_feature(nullability) 13 | #define nonnull 14 | #define nullable 15 | #define _Nullable 16 | #define _Nonnull 17 | #endif 18 | 19 | #ifndef NS_ASSUME_NONNULL_BEGIN 20 | #define NS_ASSUME_NONNULL_BEGIN 21 | #endif 22 | 23 | #ifndef NS_ASSUME_NONNULL_END 24 | #define NS_ASSUME_NONNULL_END 25 | #endif 26 | 27 | #if __has_feature(objc_generics) 28 | #define CLS_GENERIC_NSARRAY(type) NSArray 29 | #define CLS_GENERIC_NSDICTIONARY(key_type,object_key) NSDictionary 30 | #else 31 | #define CLS_GENERIC_NSARRAY(type) NSArray 32 | #define CLS_GENERIC_NSDICTIONARY(key_type,object_key) NSDictionary 33 | #endif 34 | -------------------------------------------------------------------------------- /Crashlytics.framework/Headers/CLSLogging.h: -------------------------------------------------------------------------------- 1 | // 2 | // CLSLogging.h 3 | // Crashlytics 4 | // 5 | // Copyright (c) 2015 Crashlytics, Inc. All rights reserved. 6 | // 7 | #ifdef __OBJC__ 8 | #import "CLSAttributes.h" 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | #endif 13 | 14 | 15 | 16 | /** 17 | * 18 | * The CLS_LOG macro provides as easy way to gather more information in your log messages that are 19 | * sent with your crash data. CLS_LOG prepends your custom log message with the function name and 20 | * line number where the macro was used. If your app was built with the DEBUG preprocessor macro 21 | * defined CLS_LOG uses the CLSNSLog function which forwards your log message to NSLog and CLSLog. 22 | * If the DEBUG preprocessor macro is not defined CLS_LOG uses CLSLog only. 23 | * 24 | * Example output: 25 | * -[AppDelegate login:] line 134 $ login start 26 | * 27 | * If you would like to change this macro, create a new header file, unset our define and then define 28 | * your own version. Make sure this new header file is imported after the Crashlytics header file. 29 | * 30 | * #undef CLS_LOG 31 | * #define CLS_LOG(__FORMAT__, ...) CLSNSLog... 32 | * 33 | **/ 34 | #ifdef __OBJC__ 35 | #ifdef DEBUG 36 | #define CLS_LOG(__FORMAT__, ...) CLSNSLog((@"%s line %d $ " __FORMAT__), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__) 37 | #else 38 | #define CLS_LOG(__FORMAT__, ...) CLSLog((@"%s line %d $ " __FORMAT__), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__) 39 | #endif 40 | #endif 41 | 42 | /** 43 | * 44 | * Add logging that will be sent with your crash data. This logging will not show up in the system.log 45 | * and will only be visible in your Crashlytics dashboard. 46 | * 47 | **/ 48 | 49 | #ifdef __OBJC__ 50 | OBJC_EXTERN void CLSLog(NSString *format, ...) NS_FORMAT_FUNCTION(1,2); 51 | OBJC_EXTERN void CLSLogv(NSString *format, va_list ap) NS_FORMAT_FUNCTION(1,0); 52 | 53 | /** 54 | * 55 | * Add logging that will be sent with your crash data. This logging will show up in the system.log 56 | * and your Crashlytics dashboard. It is not recommended for Release builds. 57 | * 58 | **/ 59 | OBJC_EXTERN void CLSNSLog(NSString *format, ...) NS_FORMAT_FUNCTION(1,2); 60 | OBJC_EXTERN void CLSNSLogv(NSString *format, va_list ap) NS_FORMAT_FUNCTION(1,0); 61 | 62 | 63 | NS_ASSUME_NONNULL_END 64 | #endif 65 | -------------------------------------------------------------------------------- /Crashlytics.framework/Headers/CLSReport.h: -------------------------------------------------------------------------------- 1 | // 2 | // CLSReport.h 3 | // Crashlytics 4 | // 5 | // Copyright (c) 2015 Crashlytics, Inc. All rights reserved. 6 | // 7 | 8 | #import 9 | #import "CLSAttributes.h" 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | /** 14 | * The CLSCrashReport protocol is deprecated. See the CLSReport class and the CrashyticsDelegate changes for details. 15 | **/ 16 | @protocol CLSCrashReport 17 | 18 | @property (nonatomic, copy, readonly) NSString *identifier; 19 | @property (nonatomic, copy, readonly) NSDictionary *customKeys; 20 | @property (nonatomic, copy, readonly) NSString *bundleVersion; 21 | @property (nonatomic, copy, readonly) NSString *bundleShortVersionString; 22 | @property (nonatomic, copy, readonly) NSDate *crashedOnDate; 23 | @property (nonatomic, copy, readonly) NSString *OSVersion; 24 | @property (nonatomic, copy, readonly) NSString *OSBuildVersion; 25 | 26 | @end 27 | 28 | /** 29 | * The CLSReport exposes an interface to the phsyical report that Crashlytics has created. You can 30 | * use this class to get information about the event, and can also set some values after the 31 | * event has occured. 32 | **/ 33 | @interface CLSReport : NSObject 34 | 35 | - (instancetype)init NS_UNAVAILABLE; 36 | + (instancetype)new NS_UNAVAILABLE; 37 | 38 | /** 39 | * Returns the session identifier for the report. 40 | **/ 41 | @property (nonatomic, copy, readonly) NSString *identifier; 42 | 43 | /** 44 | * Returns the custom key value data for the report. 45 | **/ 46 | @property (nonatomic, copy, readonly) NSDictionary *customKeys; 47 | 48 | /** 49 | * Returns the CFBundleVersion of the application that generated the report. 50 | **/ 51 | @property (nonatomic, copy, readonly) NSString *bundleVersion; 52 | 53 | /** 54 | * Returns the CFBundleShortVersionString of the application that generated the report. 55 | **/ 56 | @property (nonatomic, copy, readonly) NSString *bundleShortVersionString; 57 | 58 | /** 59 | * Returns the date that the report was created. 60 | **/ 61 | @property (nonatomic, copy, readonly) NSDate *dateCreated; 62 | 63 | /** 64 | * Returns the os version that the application crashed on. 65 | **/ 66 | @property (nonatomic, copy, readonly) NSString *OSVersion; 67 | 68 | /** 69 | * Returns the os build version that the application crashed on. 70 | **/ 71 | @property (nonatomic, copy, readonly) NSString *OSBuildVersion; 72 | 73 | /** 74 | * Returns YES if the report contains any crash information, otherwise returns NO. 75 | **/ 76 | @property (nonatomic, assign, readonly) BOOL isCrash; 77 | 78 | /** 79 | * You can use this method to set, after the event, additional custom keys. The rules 80 | * and semantics for this method are the same as those documented in Crashlytics.h. Be aware 81 | * that the maximum size and count of custom keys is still enforced, and you can overwrite keys 82 | * and/or cause excess keys to be deleted by using this method. 83 | **/ 84 | - (void)setObjectValue:(nullable id)value forKey:(NSString *)key; 85 | 86 | /** 87 | * Record an application-specific user identifier. See Crashlytics.h for details. 88 | **/ 89 | @property (nonatomic, copy, nullable) NSString * userIdentifier; 90 | 91 | /** 92 | * Record a user name. See Crashlytics.h for details. 93 | **/ 94 | @property (nonatomic, copy, nullable) NSString * userName; 95 | 96 | /** 97 | * Record a user email. See Crashlytics.h for details. 98 | **/ 99 | @property (nonatomic, copy, nullable) NSString * userEmail; 100 | 101 | @end 102 | 103 | NS_ASSUME_NONNULL_END 104 | -------------------------------------------------------------------------------- /Crashlytics.framework/Headers/CLSStackFrame.h: -------------------------------------------------------------------------------- 1 | // 2 | // CLSStackFrame.h 3 | // Crashlytics 4 | // 5 | // Copyright 2015 Crashlytics, Inc. All rights reserved. 6 | // 7 | 8 | #import 9 | #import "CLSAttributes.h" 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | /** 14 | * 15 | * This class is used in conjunction with -[Crashlytics recordCustomExceptionName:reason:frameArray:] to 16 | * record information about non-ObjC/C++ exceptions. All information included here will be displayed 17 | * in the Crashlytics UI, and can influence crash grouping. Be particularly careful with the use of the 18 | * address property. If set, Crashlytics will attempt symbolication and could overwrite other properities 19 | * in the process. 20 | * 21 | **/ 22 | @interface CLSStackFrame : NSObject 23 | 24 | + (instancetype)stackFrame; 25 | + (instancetype)stackFrameWithAddress:(NSUInteger)address; 26 | + (instancetype)stackFrameWithSymbol:(NSString *)symbol; 27 | 28 | @property (nonatomic, copy, nullable) NSString *symbol; 29 | @property (nonatomic, copy, nullable) NSString *library; 30 | @property (nonatomic, copy, nullable) NSString *fileName; 31 | @property (nonatomic, assign) uint32_t lineNumber; 32 | @property (nonatomic, assign) uint64_t offset; 33 | @property (nonatomic, assign) uint64_t address; 34 | 35 | @end 36 | 37 | NS_ASSUME_NONNULL_END 38 | -------------------------------------------------------------------------------- /Crashlytics.framework/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildMachineOSBuild 6 | 14F1021 7 | CFBundleDevelopmentRegion 8 | English 9 | CFBundleExecutable 10 | Crashlytics 11 | CFBundleIdentifier 12 | com.twitter.crashlytics.ios 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | Crashlytics 17 | CFBundlePackageType 18 | FMWK 19 | CFBundleShortVersionString 20 | 3.7.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleSupportedPlatforms 24 | 25 | iPhoneOS 26 | 27 | CFBundleVersion 28 | 102 29 | DTCompiler 30 | com.apple.compilers.llvm.clang.1_0 31 | DTPlatformBuild 32 | 13B137 33 | DTPlatformName 34 | iphoneos 35 | DTPlatformVersion 36 | 9.1 37 | DTSDKBuild 38 | 13B137 39 | DTSDKName 40 | iphoneos9.1 41 | DTXcode 42 | 0710 43 | DTXcodeBuild 44 | 7B91b 45 | MinimumOSVersion 46 | 6.0 47 | NSHumanReadableCopyright 48 | Copyright © 2015 Crashlytics, Inc. All rights reserved. 49 | UIDeviceFamily 50 | 51 | 1 52 | 2 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /Crashlytics.framework/Modules/module.modulemap: -------------------------------------------------------------------------------- 1 | framework module Crashlytics { 2 | header "Crashlytics.h" 3 | header "Answers.h" 4 | header "ANSCompatibility.h" 5 | header "CLSLogging.h" 6 | header "CLSReport.h" 7 | header "CLSStackFrame.h" 8 | header "CLSAttributes.h" 9 | 10 | export * 11 | 12 | link "z" 13 | link "c++" 14 | } 15 | -------------------------------------------------------------------------------- /Crashlytics.framework/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # run 4 | # 5 | # Copyright (c) 2015 Crashlytics. All rights reserved. 6 | 7 | # Figure out where we're being called from 8 | DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 9 | 10 | # Quote path in case of spaces or special chars 11 | DIR="\"${DIR}" 12 | 13 | PATH_SEP="/" 14 | VALIDATE_COMMAND="uploadDSYM\" $@ validate run-script" 15 | UPLOAD_COMMAND="uploadDSYM\" $@ run-script" 16 | 17 | # Ensure params are as expected, run in sync mode to validate 18 | eval $DIR$PATH_SEP$VALIDATE_COMMAND 19 | return_code=$? 20 | 21 | if [[ $return_code != 0 ]]; then 22 | exit $return_code 23 | fi 24 | 25 | # Verification passed, upload dSYM in background to prevent Xcode from waiting 26 | # Note: Validation is performed again before upload. 27 | # Output can still be found in Console.app 28 | eval $DIR$PATH_SEP$UPLOAD_COMMAND > /dev/null 2>&1 & 29 | -------------------------------------------------------------------------------- /Crashlytics.framework/submit: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/Crashlytics.framework/submit -------------------------------------------------------------------------------- /Crashlytics.framework/uploadDSYM: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/Crashlytics.framework/uploadDSYM -------------------------------------------------------------------------------- /Fabric.framework/Fabric: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/Fabric.framework/Fabric -------------------------------------------------------------------------------- /Fabric.framework/Headers/FABAttributes.h: -------------------------------------------------------------------------------- 1 | // 2 | // FABAttributes.h 3 | // Fabric 4 | // 5 | // Copyright (C) 2015 Twitter, Inc. 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | // 19 | 20 | #pragma once 21 | 22 | #define FAB_UNAVAILABLE(x) __attribute__((unavailable(x))) 23 | 24 | #if __has_feature(nullability) 25 | #define fab_nullable nullable 26 | #define fab_nonnull nonnull 27 | #define fab_null_unspecified null_unspecified 28 | #define fab_null_resettable null_resettable 29 | #define __fab_nullable __nullable 30 | #define __fab_nonnull __nonnull 31 | #define __fab_null_unspecified __null_unspecified 32 | #else 33 | #define fab_nullable 34 | #define fab_nonnull 35 | #define fab_null_unspecified 36 | #define fab_null_resettable 37 | #define __fab_nullable 38 | #define __fab_nonnull 39 | #define __fab_null_unspecified 40 | #endif 41 | 42 | #ifndef NS_ASSUME_NONNULL_BEGIN 43 | #define NS_ASSUME_NONNULL_BEGIN 44 | #endif 45 | 46 | #ifndef NS_ASSUME_NONNULL_END 47 | #define NS_ASSUME_NONNULL_END 48 | #endif 49 | 50 | 51 | /** 52 | * The following macros are defined here to provide 53 | * backwards compatability. If you are still using 54 | * them you should migrate to the new versions that 55 | * are defined above. 56 | */ 57 | #define FAB_NONNULL __fab_nonnull 58 | #define FAB_NULLABLE __fab_nullable 59 | #define FAB_START_NONNULL NS_ASSUME_NONNULL_BEGIN 60 | #define FAB_END_NONNULL NS_ASSUME_NONNULL_END 61 | -------------------------------------------------------------------------------- /Fabric.framework/Headers/Fabric.h: -------------------------------------------------------------------------------- 1 | // 2 | // Fabric.h 3 | // Fabric 4 | // 5 | // Copyright (C) 2015 Twitter, Inc. 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | // 19 | 20 | #import 21 | #import "FABAttributes.h" 22 | 23 | NS_ASSUME_NONNULL_BEGIN 24 | 25 | #if TARGET_OS_IPHONE 26 | #if __IPHONE_OS_VERSION_MIN_REQUIRED < 60000 27 | #error "Fabric's minimum iOS version is 6.0" 28 | #endif 29 | #else 30 | #if __MAC_OS_X_VERSION_MIN_REQUIRED < 1070 31 | #error "Fabric's minimum OS X version is 10.7" 32 | #endif 33 | #endif 34 | 35 | /** 36 | * Fabric Base. Coordinates configuration and starts all provided kits. 37 | */ 38 | @interface Fabric : NSObject 39 | 40 | /** 41 | * Initialize Fabric and all provided kits. Call this method within your App Delegate's `application:didFinishLaunchingWithOptions:` and provide the kits you wish to use. 42 | * 43 | * For example, in Objective-C: 44 | * 45 | * `[Fabric with:@[[Crashlytics class], [Twitter class], [Digits class], [MoPub class]]];` 46 | * 47 | * Swift: 48 | * 49 | * `Fabric.with([Crashlytics.self(), Twitter.self(), Digits.self(), MoPub.self()])` 50 | * 51 | * Only the first call to this method is honored. Subsequent calls are no-ops. 52 | * 53 | * @param kitClasses An array of kit Class objects 54 | * 55 | * @return Returns the shared Fabric instance. In most cases this can be ignored. 56 | */ 57 | + (instancetype)with:(NSArray *)kitClasses; 58 | 59 | /** 60 | * Returns the Fabric singleton object. 61 | */ 62 | + (instancetype)sharedSDK; 63 | 64 | /** 65 | * This BOOL enables or disables debug logging, such as kit version information. The default value is NO. 66 | */ 67 | @property (nonatomic, assign) BOOL debug; 68 | 69 | /** 70 | * Unavailable. Use `+sharedSDK` to retrieve the shared Fabric instance. 71 | */ 72 | - (id)init FAB_UNAVAILABLE("Use +sharedSDK to retrieve the shared Fabric instance."); 73 | 74 | @end 75 | 76 | NS_ASSUME_NONNULL_END 77 | 78 | -------------------------------------------------------------------------------- /Fabric.framework/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildMachineOSBuild 6 | 14F1021 7 | CFBundleDevelopmentRegion 8 | en 9 | CFBundleExecutable 10 | Fabric 11 | CFBundleIdentifier 12 | io.fabric.sdk.ios 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | Fabric 17 | CFBundlePackageType 18 | FMWK 19 | CFBundleShortVersionString 20 | 1.6.7 21 | CFBundleSignature 22 | ???? 23 | CFBundleSupportedPlatforms 24 | 25 | iPhoneOS 26 | 27 | CFBundleVersion 28 | 53 29 | DTCompiler 30 | com.apple.compilers.llvm.clang.1_0 31 | DTPlatformBuild 32 | 13C75 33 | DTPlatformName 34 | iphoneos 35 | DTPlatformVersion 36 | 9.2 37 | DTSDKBuild 38 | 13C75 39 | DTSDKName 40 | iphoneos9.2 41 | DTXcode 42 | 0721 43 | DTXcodeBuild 44 | 7C1002 45 | MinimumOSVersion 46 | 6.0 47 | NSHumanReadableCopyright 48 | Copyright © 2015 Twitter. All rights reserved. 49 | UIDeviceFamily 50 | 51 | 3 52 | 2 53 | 1 54 | 4 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /Fabric.framework/Modules/module.modulemap: -------------------------------------------------------------------------------- 1 | framework module Fabric { 2 | umbrella header "Fabric.h" 3 | 4 | export * 5 | module * { export * } 6 | } -------------------------------------------------------------------------------- /Fabric.framework/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # run 4 | # 5 | # Copyright (c) 2015 Crashlytics. All rights reserved. 6 | 7 | # Figure out where we're being called from 8 | DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 9 | 10 | # Quote path in case of spaces or special chars 11 | DIR="\"${DIR}" 12 | 13 | PATH_SEP="/" 14 | VALIDATE_COMMAND="uploadDSYM\" $@ validate run-script" 15 | UPLOAD_COMMAND="uploadDSYM\" $@ run-script" 16 | 17 | # Ensure params are as expected, run in sync mode to validate 18 | eval $DIR$PATH_SEP$VALIDATE_COMMAND 19 | return_code=$? 20 | 21 | if [[ $return_code != 0 ]]; then 22 | exit $return_code 23 | fi 24 | 25 | # Verification passed, upload dSYM in background to prevent Xcode from waiting 26 | # Note: Validation is performed again before upload. 27 | # Output can still be found in Console.app 28 | eval $DIR$PATH_SEP$UPLOAD_COMMAND > /dev/null 2>&1 & 29 | -------------------------------------------------------------------------------- /Fabric.framework/uploadDSYM: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/Fabric.framework/uploadDSYM -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 zhenwen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### v2ex 2 | v2ex iOS 客户端 3 | 4 | ### 运行条件 5 | Swift 2.2 & Xcode 7.3 6 | 7 | ### 应用程序截图 8 | ![ScreenShot](https://raw.githubusercontent.com/shitoudev/v2ex/master/ScreenShot/s_1.jpg) 9 | ![ScreenShot](https://raw.githubusercontent.com/shitoudev/v2ex/master/ScreenShot/s_2.jpg) 10 | 11 | ### 未完成列表 12 | * ~~我的 界面~~ 13 | * ~~today 扩展~~ 14 | * ~~应用内通知提醒~~ 15 | * ~~支持iOS9 Spotlight 搜索应用内阅读过的内容~~ 16 | * ~~支持扩展程序,但是获取数据不够完美~~ 17 | * ~~找回密码页面~~ 18 | * 帖子阅读标记 19 | 20 | ### 致谢 21 | 感谢 [@Mysdes](https://twitter.com/Mysdes) 设计的应用图标 22 | 23 | 还有伟大的开源社区及苹果给我们带来这么好用的产品! 24 | 25 | ### 联系 26 | shitoudev#gmail.com 27 | 28 | ### License 29 | [MIT license.](http://www.opensource.org/licenses/mit-license.php) 30 | -------------------------------------------------------------------------------- /ScreenShot/s_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/ScreenShot/s_1.jpg -------------------------------------------------------------------------------- /ScreenShot/s_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/ScreenShot/s_2.jpg -------------------------------------------------------------------------------- /v2ex.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /v2ex.xcodeproj/xcshareddata/xcbaselines/97BA7CEC1AF47FBB00950A0C.xcbaseline/D1287C0B-8F0F-41B0-83DA-12EE4B5C41B0.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | v2exTests 8 | 9 | testPerformanceExample() 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 8.546e-07 15 | baselineIntegrationDisplayName 16 | Local Baseline 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /v2ex.xcodeproj/xcshareddata/xcbaselines/97BA7CEC1AF47FBB00950A0C.xcbaseline/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | runDestinationsByUUID 6 | 7 | D1287C0B-8F0F-41B0-83DA-12EE4B5C41B0 8 | 9 | localComputer 10 | 11 | busSpeedInMHz 12 | 100 13 | cpuCount 14 | 1 15 | cpuKind 16 | Intel Core i5 17 | cpuSpeedInMHz 18 | 2600 19 | logicalCPUCoresPerPackage 20 | 4 21 | modelCode 22 | MacBookPro11,1 23 | physicalCPUCoresPerPackage 24 | 2 25 | platformIdentifier 26 | com.apple.platform.macosx 27 | 28 | targetArchitecture 29 | x86_64 30 | targetDevice 31 | 32 | modelCode 33 | iPhone8,1 34 | platformIdentifier 35 | com.apple.platform.iphonesimulator 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /v2ex/APIManage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIManage.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 5/24/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import Alamofire 10 | import Kanna 11 | 12 | class APIManage: Manager { 13 | 14 | // enum Router: URLRequestConvertible { 15 | 16 | static let domain = "v2ex.com" 17 | static let baseURLString = "http://www." + APIManage.domain + "/" 18 | 19 | struct Router { 20 | 21 | static var ApiLatest: String { 22 | return APIManage.baseURLString + "api/topics/latest.json" 23 | } 24 | 25 | static var ApiComment: String { 26 | return APIManage.baseURLString + "api/replies/show.json?topic_id=" 27 | } 28 | 29 | static var ApiMember: String { 30 | return APIManage.baseURLString + "api/members/show.json" 31 | } 32 | 33 | static var ApiTopic: String { 34 | return APIManage.baseURLString + "api/topics/show.json?id=" 35 | } 36 | 37 | static var Navi: String { 38 | return APIManage.baseURLString + "?tab=" 39 | } 40 | 41 | static var Node: String { 42 | return APIManage.baseURLString + "go/" 43 | } 44 | 45 | static var Signin: String { 46 | return APIManage.baseURLString + "signin" 47 | } 48 | 49 | static var Post: String { 50 | return APIManage.baseURLString + "t/" 51 | } 52 | 53 | static var Member: String { 54 | return APIManage.baseURLString + "member/" 55 | } 56 | 57 | static var Notification: String { 58 | return APIManage.baseURLString + "notifications" 59 | } 60 | static var FindPwd: String { 61 | return APIManage.baseURLString + "forgot" 62 | } 63 | static var Captcha: String { 64 | return APIManage.baseURLString + "_captcha" 65 | } 66 | 67 | } 68 | 69 | /** 70 | Creates default values for the "Accept-Encoding", "Accept-Language" and "User-Agent" headers. 71 | */ 72 | static let HTTPHeaders: [String: String] = { 73 | // Accept-Encoding HTTP Header; see https://tools.ietf.org/html/rfc7230#section-4.2.3 74 | let acceptEncoding: String = "gzip;q=1.0,compress;q=0.5" 75 | 76 | // Accept-Language HTTP Header; see https://tools.ietf.org/html/rfc7231#section-5.3.5 77 | let acceptLanguage: String = { 78 | var components: [String] = [] 79 | for (index, languageCode) in (NSLocale.preferredLanguages() as [String]).enumerate() { 80 | let q = 1.0 - (Double(index) * 0.1) 81 | components.append("\(languageCode);q=\(q)") 82 | if q <= 0.5 { 83 | break 84 | } 85 | } 86 | 87 | return components.joinWithSeparator(",") 88 | }() 89 | 90 | // User-Agent Header; see https://tools.ietf.org/html/rfc7231#section-5.5.3 91 | let userAgent: String = { 92 | if let info = NSBundle.mainBundle().infoDictionary { 93 | let executable: AnyObject = info[kCFBundleExecutableKey as String] ?? "Unknown" 94 | let bundle: AnyObject = info[kCFBundleIdentifierKey as String] ?? "Unknown" 95 | let version: AnyObject = info[kCFBundleVersionKey as String] ?? "Unknown" 96 | let os: AnyObject = NSProcessInfo.processInfo().operatingSystemVersionString ?? "Unknown" 97 | 98 | var mutableUserAgent = NSMutableString(string: "\(executable)/\(bundle) (\(version); OS \(os))") as CFMutableString 99 | let transform = NSString(string: "Any-Latin; Latin-ASCII; [:^ASCII:] Remove") as CFString 100 | 101 | if CFStringTransform(mutableUserAgent, UnsafeMutablePointer(nil), transform, false) { 102 | return mutableUserAgent as String 103 | } 104 | } 105 | 106 | return "Alamofire" 107 | }() 108 | 109 | return [ 110 | "Accept-Encoding": acceptEncoding, 111 | "Accept-Language": acceptLanguage, 112 | "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 9_0 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Mobile/13A344" 113 | ] 114 | }() 115 | 116 | internal static let sharedManager: APIManage = { 117 | 118 | let cookiesStorage = NSHTTPCookieStorage.sharedHTTPCookieStorage() 119 | let configuration: NSURLSessionConfiguration = NSURLSessionConfiguration.defaultSessionConfiguration() 120 | configuration.HTTPCookieStorage = cookiesStorage 121 | configuration.HTTPCookieAcceptPolicy = NSHTTPCookieAcceptPolicy.Always 122 | configuration.HTTPAdditionalHeaders = APIManage.HTTPHeaders //APIManage.defaultHTTPHeaders 123 | 124 | return APIManage(configuration: configuration) 125 | }() 126 | 127 | /** 128 | 获取once 129 | 130 | :param: respStr 返回的 html string 131 | 132 | :returns: once string 133 | */ 134 | static func getOnceStringFromHtmlResponse(respStr: String) -> String? { 135 | var once: String? 136 | guard let doc = HTML(html: respStr, encoding: NSUTF8StringEncoding) else { 137 | return once 138 | } 139 | if let onceNode = doc.body!.at_css("input[name='once']"), valueText = onceNode["value"] { 140 | once = valueText 141 | } 142 | return once 143 | } 144 | 145 | } -------------------------------------------------------------------------------- /v2ex/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 5/2/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import v2exKit 11 | import Fabric 12 | import Crashlytics 13 | 14 | public let _dismissAfter = 1.65 15 | 16 | public let v2exUserLoginSuccessNotification = "shitou.v2exUserLoginSuccessNotification" 17 | public let v2exUserLogoutSuccessNotification = "shitou.v2exUserLogoutSuccessNotification" 18 | 19 | @UIApplicationMain 20 | class AppDelegate: UIResponder, UIApplicationDelegate { 21 | 22 | var window: UIWindow? 23 | /// 需要跳转的VC 24 | var openURLQueue: [UIViewController] = [] 25 | /// 应用是否处于活动中 26 | var appActiveing = false 27 | 28 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 29 | 30 | let navigationBar = UINavigationBar.appearance() 31 | navigationBar.barTintColor = kAppNormalColor 32 | navigationBar.tintColor = UIColor.whiteColor() 33 | navigationBar.titleTextAttributes = [NSForegroundColorAttributeName:UIColor.whiteColor()] 34 | 35 | UITabBar.appearance().tintColor = kAppNormalColor 36 | 37 | application.statusBarStyle = UIStatusBarStyle.LightContent 38 | window?.tintColor = kAppNormalColor 39 | 40 | NotificationManage.sharedManager 41 | 42 | if MemberModel.sharedMember.isLogin() { 43 | print("登录中") 44 | } else { 45 | print("未登录") 46 | NotificationManage.sharedManager.timerStop() 47 | } 48 | // MemberModel.sharedMember.removeUserData() 49 | application.registerUserNotificationSettings(UIUserNotificationSettings(forTypes: [.Badge, .Alert, .Sound], categories: nil)) 50 | 51 | Fabric.with([Crashlytics.self]) 52 | Flurry.startSession("4PJF88FR8SBPJBV3R69V") 53 | 54 | // Override point for customization after application launch. 55 | return true 56 | } 57 | 58 | 59 | func applicationWillResignActive(application: UIApplication) { 60 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 61 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 62 | if MemberModel.sharedMember.isLogin() { 63 | NotificationManage.sharedManager.timerStop() 64 | } 65 | appActiveing = false 66 | print("applicationWillResignActive") 67 | } 68 | 69 | func applicationDidEnterBackground(application: UIApplication) { 70 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 71 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 72 | print("applicationDidEnterBackground") 73 | } 74 | 75 | func applicationWillEnterForeground(application: UIApplication) { 76 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 77 | print("applicationWillEnterForeground") 78 | } 79 | 80 | func applicationDidBecomeActive(application: UIApplication) { 81 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 82 | 83 | pushToViewController() 84 | 85 | if MemberModel.sharedMember.isLogin() { 86 | NotificationManage.sharedManager.timerRestart() 87 | } 88 | appActiveing = true 89 | print("applicationDidBecomeActive") 90 | } 91 | 92 | func applicationWillTerminate(application: UIApplication) { 93 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 94 | } 95 | 96 | func application(app: UIApplication, openURL url: NSURL, options: [String : AnyObject]) -> Bool { 97 | return handlerURL(url) 98 | } 99 | 100 | func application(application: UIApplication, willContinueUserActivityWithType userActivityType: String) -> Bool { 101 | return userActivityType == "com.apple.corespotlightitem" 102 | } 103 | 104 | func application(application: UIApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: ([AnyObject]?) -> Void) -> Bool { 105 | print("continueUserActivity") 106 | let identifier = userActivity.userInfo?["kCSSearchableItemActivityIdentifier"] as! String 107 | let url = NSURL(string: identifier)! 108 | return handlerURL(url) 109 | } 110 | 111 | func handlerURL(url: NSURL) -> Bool { 112 | print("url = \(url)") 113 | let urlComponents = NSURLComponents(URL: url, resolvingAgainstBaseURL: true)! 114 | guard let host = urlComponents.host, queryItems = urlComponents.queryItems where host == "post" else { 115 | return false 116 | } 117 | var postId = 0 118 | for item in queryItems { 119 | if let val = item.value where item.name == "postId" { 120 | postId = (val as NSString).integerValue 121 | break 122 | } 123 | } 124 | guard postId > 0 else { 125 | return false 126 | } 127 | openURLQueue.removeAll(keepCapacity: true) 128 | let viewController = PostDetailViewController().allocWithRouterParams(nil) 129 | viewController.postId = postId 130 | openURLQueue.append(viewController) 131 | if appActiveing { 132 | pushToViewController() 133 | } 134 | return true 135 | } 136 | 137 | func pushToViewController() { 138 | guard openURLQueue.count > 0 else { 139 | return 140 | } 141 | let application = UIApplication.sharedApplication() 142 | let viewController = openURLQueue.first! 143 | let tabbarController = application.keyWindow?.rootViewController as! UITabBarController 144 | if let selectedViewController = tabbarController.selectedViewController as? UINavigationController { 145 | selectedViewController.pushViewController(viewController, animated: true) 146 | openURLQueue.removeAll(keepCapacity: true) 147 | } 148 | } 149 | } 150 | 151 | -------------------------------------------------------------------------------- /v2ex/AtUserTableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AtUserTableView.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 7/1/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import v2exKit 11 | 12 | protocol AtUserTableViewDelegate: NSObjectProtocol { 13 | func didSelectedUser(user: MemberModel) 14 | } 15 | 16 | class AtUserTableView: UITableView { 17 | 18 | var originData: [MemberModel]! 19 | var dataSouce: [MemberModel] = [MemberModel]() { 20 | didSet { 21 | self.reloadData() 22 | } 23 | } 24 | var searchText: String! 25 | weak var atDelegate: AtUserTableViewDelegate? 26 | 27 | override init(frame: CGRect, style: UITableViewStyle) { 28 | super.init(frame: frame, style: style) 29 | 30 | dataSource = self 31 | delegate = self 32 | rowHeight = 44 33 | registerNib(UINib(nibName: "MemberCell", bundle: nil), forCellReuseIdentifier: "memberCellId") 34 | tableFooterView = defaultTableFooterView 35 | } 36 | 37 | required init(coder aDecoder: NSCoder) { 38 | fatalError("init(coder:) has not been implemented") 39 | } 40 | 41 | func searchMember() -> Bool { 42 | let namePredicate = NSPredicate(format: "username contains[c] %@", self.searchText) 43 | dataSouce = originData.filter({ (user: MemberModel) -> Bool in 44 | return namePredicate.evaluateWithObject(user) 45 | }) 46 | 47 | return dataSouce.count > 0 48 | } 49 | 50 | } 51 | 52 | // MARK: UITableViewDataSource & UITableViewDelegate 53 | extension AtUserTableView: UITableViewDelegate, UITableViewDataSource { 54 | // UITableViewDataSource 55 | func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 56 | 57 | let cell: MemberCell = tableView.dequeueReusableCellWithIdentifier("memberCellId") as! MemberCell 58 | 59 | let member = dataSouce[indexPath.row] 60 | cell.updateCell(member) 61 | 62 | return cell 63 | } 64 | 65 | func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 66 | return dataSouce.count; 67 | } 68 | 69 | // UITableViewDelegate 70 | func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { 71 | atDelegate?.didSelectedUser(dataSouce[indexPath.row]) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /v2ex/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /v2ex/BaseViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseViewController.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 5/11/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Kingfisher 11 | 12 | class BaseViewController: UIViewController { 13 | 14 | var loadingView: UIActivityIndicatorView! 15 | var reloadView: UIButton! 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | // 加载中 21 | self.loadingView = UIActivityIndicatorView(activityIndicatorStyle: UIActivityIndicatorViewStyle.Gray) 22 | loadingView.center = self.view.center 23 | loadingView.hidesWhenStopped = true 24 | self.view.addSubview(loadingView) 25 | // 重新加载按钮 26 | 27 | self.reloadView = UIButton(type: .Custom) 28 | reloadView.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: 100, height: 40)) 29 | reloadView.center = self.view.center 30 | reloadView.setTitle("点我重新加载", forState: UIControlState.Normal) 31 | reloadView.setTitleColor(UIColor.colorWithHexString("#333344"), forState: UIControlState.Normal) 32 | reloadView.titleLabel?.font = UIFont.systemFontOfSize(13.0) 33 | reloadView.hidden = true 34 | reloadView.addTarget(self, action: #selector(reloadViewTapped(_:)), forControlEvents: UIControlEvents.TouchUpInside) 35 | self.view.addSubview(reloadView) 36 | } 37 | 38 | func reloadViewTapped(sender: UIButton!) { 39 | 40 | } 41 | 42 | override func viewDidAppear(animated: Bool) { 43 | super.viewDidAppear(animated) 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /v2ex/CommentCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommentCell.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 6/8/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import TTTAttributedLabel 11 | import v2exKit 12 | 13 | let usernameRegularExpression = try! NSRegularExpression(pattern: "@[^.\"?]((?!\\.)\\w){2,}", options: NSRegularExpressionOptions.CaseInsensitive) 14 | let httpRegularExpression = try! NSRegularExpression(pattern: "(?:https?|ftp|file)://[\\w+?&#/%=~\\-|@$?!:,.]*", options: NSRegularExpressionOptions.CaseInsensitive) 15 | 16 | //let httpRegularExpression = NSRegularExpression(pattern: "(?])\\b(?:(?:https?|ftp|file)://|[a-z]\\.)[-A-Z0-9+&#/%=~_|$?!:,.]*[A-Z0-9+&#/%=~_|$]", options: NSRegularExpressionOptions.CaseInsensitive, error: nil)! 17 | 18 | class CommentCell: UITableViewCell { 19 | @IBOutlet weak var avatarButton: UIButton! 20 | @IBOutlet weak var contentLabel: TTTAttributedLabel! 21 | @IBOutlet weak var usernameButton: UIButton! 22 | @IBOutlet weak var timeLabel: UILabel! 23 | 24 | var isButtonAddTarget = false 25 | 26 | override func awakeFromNib() { 27 | super.awakeFromNib() 28 | 29 | layoutMargins = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 0) 30 | separatorInset = UIEdgeInsetsZero 31 | selectionStyle = UITableViewCellSelectionStyle.None 32 | avatarButton.layer.cornerRadius = 5 33 | avatarButton.layer.masksToBounds = true 34 | timeLabel.textColor = UIColor.grayColor() 35 | timeLabel.font = UIFont.systemFontOfSize(12) 36 | usernameButton.contentHorizontalAlignment = UIControlContentHorizontalAlignment.Left 37 | usernameButton.setTitleColor(UIColor.colorWithHexString(kLinkColor), forState: .Normal) 38 | 39 | var linkAttributes = Dictionary() 40 | linkAttributes[kCTForegroundColorAttributeName as String] = UIColor.colorWithHexString(kLinkColor).CGColor 41 | contentLabel.linkAttributes = linkAttributes 42 | contentLabel.extendsLinkTouchArea = false 43 | contentLabel.font = kContentFont 44 | } 45 | 46 | func updateCell(comment: CommentModel) -> Void { 47 | avatarButton.kf_setImageWithURL(NSURL(string: comment.member.avatar_large)!, forState: .Normal, placeholderImage: nil) 48 | usernameButton.setTitle(comment.member.username, forState: .Normal) 49 | timeLabel.text = comment.getSmartTime() 50 | 51 | let content = comment.apiData ? comment.content : comment.getContent() 52 | if comment.linkMatched { 53 | contentLabel.setText(content, afterInheritingLabelAttributesAndConfiguringWithBlock: { (mutableAttributedString) -> NSMutableAttributedString! in 54 | if comment.linkRange?.count > 0 { 55 | for range in comment.linkRange! { 56 | addLinkAttributed(mutableAttributedString, range: range) 57 | } 58 | } 59 | return mutableAttributedString 60 | }) 61 | } else { 62 | var linkRange = [NSRange]() 63 | 64 | contentLabel.setText(content, afterInheritingLabelAttributesAndConfiguringWithBlock: { (mutableAttributedString) -> NSMutableAttributedString! in 65 | 66 | let stringRange = NSMakeRange(0, mutableAttributedString.length) 67 | // username 68 | usernameRegularExpression.enumerateMatchesInString(mutableAttributedString.string, options: NSMatchingOptions.ReportCompletion, range: stringRange, usingBlock: { (result, flags, stop) -> Void in 69 | 70 | if let resultVal = result { 71 | addLinkAttributed(mutableAttributedString, range: resultVal.range) 72 | linkRange.append(resultVal.range) 73 | } 74 | }) 75 | // http link 76 | httpRegularExpression.enumerateMatchesInString(mutableAttributedString.string, options: NSMatchingOptions.ReportCompletion, range: stringRange, usingBlock: { (result, flags, stop) -> Void in 77 | 78 | if let resultVal = result { 79 | addLinkAttributed(mutableAttributedString, range: resultVal.range) 80 | linkRange.append(resultVal.range) 81 | } 82 | // println("result = \(result), flags = \(flags)") 83 | }) 84 | 85 | return mutableAttributedString 86 | }) 87 | 88 | comment.linkRange = linkRange 89 | comment.linkMatched = true 90 | } 91 | 92 | if comment.linkRange?.count > 0 { 93 | for range in comment.linkRange! { 94 | let linkStr = (content as NSString).substringWithRange(range) 95 | contentLabel.addLinkToURL(NSURL(string: linkStr), withRange: range) 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /v2ex/CommentCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 24 | 34 | 45 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /v2ex/CommentModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommentModel.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 6/8/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | import SwiftyJSON 12 | import Kanna 13 | import v2exKit 14 | 15 | class CommentModel: NSObject { 16 | 17 | var comment_id: Int 18 | var content: String 19 | var member: MemberModel 20 | var linkRange: [NSRange]?, linkMatched = false, contentRegexed = false, apiData = true 21 | var smart_time: String?, content_rendered: String?, created: Int?, last_modified: Int?, thanks: Int? 22 | 23 | init(fromDictionary dictionary: NSDictionary) { 24 | self.comment_id = dictionary["id"] as! Int 25 | self.content = dictionary["content"] as! String //!.stringValue 26 | 27 | self.content_rendered = dictionary["content_rendered"] as? String 28 | self.smart_time = dictionary["smart_time"] as? String 29 | self.created = dictionary["created"] as? Int 30 | self.last_modified = dictionary["last_modified"] as? Int 31 | self.thanks = dictionary["thanks"] as? Int 32 | 33 | self.member = MemberModel(fromDictionary: dictionary["member"] as! NSDictionary) 34 | } 35 | 36 | func getContent() -> String { 37 | if !contentRegexed { 38 | self.content = content.stringByReplacingOccurrencesOfString("<[^>]+>", withString: "", options: .RegularExpressionSearch, range: nil) 39 | self.contentRegexed = true 40 | } 41 | return content 42 | } 43 | 44 | func getSmartTime() -> String { 45 | if smart_time != nil { 46 | return smart_time! 47 | } else { 48 | return created == nil ? "" : String.smartDate(Double(self.created!)) 49 | } 50 | } 51 | 52 | /** 53 | 通过接口获取评论 54 | 55 | :param: postId 主题ID 56 | :param: completionHandler 回调 57 | */ 58 | static func getComments(postId:Int, salt: String, completionHandler:(obj: [CommentModel], NSError?)->Void) { 59 | 60 | let url = APIManage.Router.ApiComment + String(postId) + salt 61 | 62 | var result = [CommentModel]() 63 | Alamofire.request(.GET, url).responseJSON(options: .AllowFragments) { (response) -> Void in 64 | 65 | if response.result.isSuccess { 66 | let json = JSON(response.result.value!).arrayValue 67 | for item in json { 68 | let comment = CommentModel(fromDictionary: item.dictionaryObject!) 69 | result.append(comment) 70 | } 71 | completionHandler(obj: result, nil) 72 | }else{ 73 | let err = NSError(domain: APIManage.domain, code: 202, userInfo: [NSLocalizedDescriptionKey:"数据获取失败"]) 74 | completionHandler(obj: [], err) 75 | } 76 | 77 | } 78 | 79 | } 80 | 81 | /** 82 | 通过网页获取评论 83 | 84 | :param: postId 主题ID 85 | :param: page 当前页数 86 | :param: completionHandler 回调 87 | */ 88 | static func getCommentsFromHtml(postId: Int, page: Int, completionHandler:(obj: [CommentModel], NSError?)->Void) { 89 | let url = APIManage.Router.Post + String(postId) + "?p=\(page)" 90 | var result = [CommentModel]() 91 | let mgr = APIManage.sharedManager 92 | mgr.request(.GET, url, parameters: nil).responseString(encoding: nil, completionHandler: { (response) -> Void in 93 | 94 | if response.result.isSuccess { 95 | result = self.getCommentsFromHtmlResponse(response.result.value!) 96 | completionHandler(obj: result, nil) 97 | } else { 98 | let err = NSError(domain: APIManage.domain, code: 202, userInfo: [NSLocalizedDescriptionKey:"数据获取失败"]) 99 | completionHandler(obj: [], err) 100 | } 101 | }) 102 | } 103 | 104 | /** 105 | 解析html 返回评论 106 | 107 | :param: respStr html string 108 | 109 | :returns: post 数组 110 | */ 111 | private static func getCommentsFromHtmlResponse(respStr: String) -> [CommentModel] { 112 | var result = [CommentModel]() 113 | guard let doc = HTML(html: respStr, encoding: NSUTF8StringEncoding) else { 114 | return result 115 | } 116 | 117 | for div in doc.body!.css("div[class='cell']") { 118 | if let table = div.at_css("table"), divId = div["id"], avatarNode = table.at_css("img[class='avatar']") { 119 | var comment_id = 0, avatar = "", content = "", username = "", smart_time = "" 120 | avatar = avatarNode["src"]! 121 | // comment id 122 | let components = divId.componentsSeparatedByString("_") 123 | if let lastStr = components.last { 124 | comment_id = (lastStr as NSString).integerValue 125 | } 126 | // username 127 | if let usernameNode = table.at_css("a[class='dark']"), nameText = usernameNode.text { 128 | username = nameText 129 | } 130 | // content 131 | if let contentNode = table.at_css("div[class='reply_content']"), text = contentNode.text { 132 | content = text 133 | } 134 | // smart time 135 | if let timeNode = table.at_css("span[class='fade small']"), timeText = timeNode.text { 136 | smart_time = timeText 137 | } 138 | let comment = ["id":comment_id, "content":content, "smart_time":smart_time, "member":["username":username, "avatar_large":avatar]] 139 | let commentModel = CommentModel(fromDictionary: comment) 140 | commentModel.apiData = false 141 | result.append(commentModel) 142 | } 143 | } 144 | return result 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /v2ex/HotViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HotViewController.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 5/2/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import v2exKit 11 | 12 | class HotViewController: BaseViewController { 13 | 14 | var tableView: PostTableView! 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | // Do any additional setup after loading the view, typically from a nib. 19 | self.title = "最热" 20 | 21 | self.tableView = PostTableView(frame: view.bounds, style: .Plain) 22 | tableView.dataType = PostType.Navi 23 | tableView.target = "hot" 24 | self.view = tableView 25 | } 26 | 27 | override func viewWillAppear(animated: Bool) { 28 | super.viewWillAppear(animated) 29 | 30 | tableView.deselectRow() 31 | } 32 | 33 | override func didReceiveMemoryWarning() { 34 | super.didReceiveMemoryWarning() 35 | // Dispose of any resources that can be recreated. 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /v2ex/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "29x29", 5 | "idiom" : "iphone", 6 | "filename" : "icon29@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "29x29", 11 | "idiom" : "iphone", 12 | "filename" : "icon29@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "40x40", 17 | "idiom" : "iphone", 18 | "filename" : "icon40@2x.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "40x40", 23 | "idiom" : "iphone", 24 | "filename" : "icon40@3x.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "60x60", 29 | "idiom" : "iphone", 30 | "filename" : "icon60@2x.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "60x60", 35 | "idiom" : "iphone", 36 | "filename" : "icon60@3x.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "idiom" : "ipad", 41 | "size" : "29x29", 42 | "scale" : "1x" 43 | }, 44 | { 45 | "size" : "29x29", 46 | "idiom" : "ipad", 47 | "filename" : "icon29@2x.png", 48 | "scale" : "2x" 49 | }, 50 | { 51 | "idiom" : "ipad", 52 | "size" : "40x40", 53 | "scale" : "1x" 54 | }, 55 | { 56 | "size" : "40x40", 57 | "idiom" : "ipad", 58 | "filename" : "icon40@2x.png", 59 | "scale" : "2x" 60 | }, 61 | { 62 | "size" : "76x76", 63 | "idiom" : "ipad", 64 | "filename" : "icon76.png", 65 | "scale" : "1x" 66 | }, 67 | { 68 | "size" : "76x76", 69 | "idiom" : "ipad", 70 | "filename" : "icon76@2x.png", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "size" : "83.5x83.5", 75 | "idiom" : "ipad", 76 | "filename" : "icon83.5@2x.png", 77 | "scale" : "2x" 78 | } 79 | ], 80 | "info" : { 81 | "version" : 1, 82 | "author" : "xcode" 83 | } 84 | } -------------------------------------------------------------------------------- /v2ex/Images.xcassets/AppIcon.appiconset/icon29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/v2ex/Images.xcassets/AppIcon.appiconset/icon29@2x.png -------------------------------------------------------------------------------- /v2ex/Images.xcassets/AppIcon.appiconset/icon29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/v2ex/Images.xcassets/AppIcon.appiconset/icon29@3x.png -------------------------------------------------------------------------------- /v2ex/Images.xcassets/AppIcon.appiconset/icon40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/v2ex/Images.xcassets/AppIcon.appiconset/icon40@2x.png -------------------------------------------------------------------------------- /v2ex/Images.xcassets/AppIcon.appiconset/icon40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/v2ex/Images.xcassets/AppIcon.appiconset/icon40@3x.png -------------------------------------------------------------------------------- /v2ex/Images.xcassets/AppIcon.appiconset/icon60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/v2ex/Images.xcassets/AppIcon.appiconset/icon60@2x.png -------------------------------------------------------------------------------- /v2ex/Images.xcassets/AppIcon.appiconset/icon60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/v2ex/Images.xcassets/AppIcon.appiconset/icon60@3x.png -------------------------------------------------------------------------------- /v2ex/Images.xcassets/AppIcon.appiconset/icon76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/v2ex/Images.xcassets/AppIcon.appiconset/icon76.png -------------------------------------------------------------------------------- /v2ex/Images.xcassets/AppIcon.appiconset/icon76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/v2ex/Images.xcassets/AppIcon.appiconset/icon76@2x.png -------------------------------------------------------------------------------- /v2ex/Images.xcassets/AppIcon.appiconset/icon83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/v2ex/Images.xcassets/AppIcon.appiconset/icon83.5@2x.png -------------------------------------------------------------------------------- /v2ex/Images.xcassets/first.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "first.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /v2ex/Images.xcassets/first.imageset/first.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/v2ex/Images.xcassets/first.imageset/first.pdf -------------------------------------------------------------------------------- /v2ex/Images.xcassets/homeTabbar.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x", 10 | "filename" : "homeTabbar@2x.png" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x", 15 | "filename" : "homeTabbar@3x.png" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /v2ex/Images.xcassets/homeTabbar.imageset/homeTabbar@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/v2ex/Images.xcassets/homeTabbar.imageset/homeTabbar@2x.png -------------------------------------------------------------------------------- /v2ex/Images.xcassets/homeTabbar.imageset/homeTabbar@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/v2ex/Images.xcassets/homeTabbar.imageset/homeTabbar@3x.png -------------------------------------------------------------------------------- /v2ex/Images.xcassets/hotTabbar.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x", 10 | "filename" : "hot@2x.png" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x", 15 | "filename" : "hot@3x.png" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /v2ex/Images.xcassets/hotTabbar.imageset/hot@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/v2ex/Images.xcassets/hotTabbar.imageset/hot@2x.png -------------------------------------------------------------------------------- /v2ex/Images.xcassets/hotTabbar.imageset/hot@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/v2ex/Images.xcassets/hotTabbar.imageset/hot@3x.png -------------------------------------------------------------------------------- /v2ex/Images.xcassets/latestTabbar.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x", 10 | "filename" : "latest@2x.png" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x", 15 | "filename" : "latest@3x.png" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /v2ex/Images.xcassets/latestTabbar.imageset/latest@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/v2ex/Images.xcassets/latestTabbar.imageset/latest@2x.png -------------------------------------------------------------------------------- /v2ex/Images.xcassets/latestTabbar.imageset/latest@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/v2ex/Images.xcassets/latestTabbar.imageset/latest@3x.png -------------------------------------------------------------------------------- /v2ex/Images.xcassets/nodeTabbar.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x", 10 | "filename" : "nodeTabbar@2x.png" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x", 15 | "filename" : "nodeTabbar@3x.png" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /v2ex/Images.xcassets/nodeTabbar.imageset/nodeTabbar@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/v2ex/Images.xcassets/nodeTabbar.imageset/nodeTabbar@2x.png -------------------------------------------------------------------------------- /v2ex/Images.xcassets/nodeTabbar.imageset/nodeTabbar@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/v2ex/Images.xcassets/nodeTabbar.imageset/nodeTabbar@3x.png -------------------------------------------------------------------------------- /v2ex/Images.xcassets/second.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "second.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /v2ex/Images.xcassets/second.imageset/second.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/v2ex/Images.xcassets/second.imageset/second.pdf -------------------------------------------------------------------------------- /v2ex/Images.xcassets/webBack.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x", 10 | "filename" : "webBack@2x.png" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /v2ex/Images.xcassets/webBack.imageset/webBack@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/v2ex/Images.xcassets/webBack.imageset/webBack@2x.png -------------------------------------------------------------------------------- /v2ex/Images.xcassets/webForward.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x", 10 | "filename" : "webForward@2x.png" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /v2ex/Images.xcassets/webForward.imageset/webForward@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/v2ex/Images.xcassets/webForward.imageset/webForward@2x.png -------------------------------------------------------------------------------- /v2ex/Images.xcassets/webMore.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x", 10 | "filename" : "webMore@2x.png" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /v2ex/Images.xcassets/webMore.imageset/webMore@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/v2ex/Images.xcassets/webMore.imageset/webMore@2x.png -------------------------------------------------------------------------------- /v2ex/Images.xcassets/webRefresh.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x", 10 | "filename" : "webRefresh@2x.png" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /v2ex/Images.xcassets/webRefresh.imageset/webRefresh@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/v2ex/Images.xcassets/webRefresh.imageset/webRefresh@2x.png -------------------------------------------------------------------------------- /v2ex/Images.xcassets/webStop.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x", 10 | "filename" : "webStop@2x.png" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /v2ex/Images.xcassets/webStop.imageset/webStop@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/v2ex/Images.xcassets/webStop.imageset/webStop@2x.png -------------------------------------------------------------------------------- /v2ex/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | zh_CN 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleURLTypes 22 | 23 | 24 | CFBundleTypeRole 25 | Editor 26 | CFBundleURLName 27 | cc.shitoudev.v2ex 28 | CFBundleURLSchemes 29 | 30 | v2ex 31 | 32 | 33 | 34 | CFBundleVersion 35 | 16032417 36 | Fabric 37 | 38 | APIKey 39 | 7b53c2e05af000ba52880507b237ee822aadb974 40 | Kits 41 | 42 | 43 | KitInfo 44 | 45 | KitName 46 | Crashlytics 47 | 48 | 49 | 50 | LSApplicationQueriesSchemes 51 | 52 | googlechrome 53 | 54 | LSRequiresIPhoneOS 55 | 56 | NSAppTransportSecurity 57 | 58 | NSAllowsArbitraryLoads 59 | 60 | NSExceptionDomains 61 | 62 | v2ex.co 63 | 64 | NSExceptionAllowsInsecureHTTPLoads 65 | 66 | NSExceptionMinimumTLSVersion 67 | TLSv1.1 68 | NSIncludesSubdomains 69 | 70 | 71 | v2ex.com 72 | 73 | NSExceptionAllowsInsecureHTTPLoads 74 | 75 | NSExceptionMinimumTLSVersion 76 | TLSv1.1 77 | NSIncludesSubdomains 78 | 79 | 80 | 81 | 82 | UILaunchStoryboardName 83 | LaunchScreen 84 | UIMainStoryboardFile 85 | Main 86 | UIRequiredDeviceCapabilities 87 | 88 | armv7 89 | 90 | UIStatusBarTintParameters 91 | 92 | UINavigationBar 93 | 94 | Style 95 | UIBarStyleDefault 96 | Translucent 97 | 98 | 99 | 100 | UISupportedInterfaceOrientations 101 | 102 | UIInterfaceOrientationPortrait 103 | UIInterfaceOrientationLandscapeLeft 104 | UIInterfaceOrientationLandscapeRight 105 | UIInterfaceOrientationPortraitUpsideDown 106 | 107 | UIViewControllerBasedStatusBarAppearance 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /v2ex/JSONAble.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONAble.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 5/2/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class JSONAble: NSObject { 12 | public class func fromJSON(_: [String:AnyObject]) -> JSONAble { 13 | return JSONAble() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /v2ex/LatestViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LatestViewController.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 5/8/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import v2exKit 11 | 12 | class LatestViewController: BaseViewController { 13 | 14 | var tableView: PostTableView! 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | // Do any additional setup after loading the view, typically from a nib. 19 | self.title = "最新" 20 | 21 | self.tableView = PostTableView(frame: view.bounds, style: .Plain) 22 | tableView.dataType = PostType.Api 23 | tableView.target = "latest" 24 | self.view = tableView 25 | } 26 | 27 | override func viewWillAppear(animated: Bool) { 28 | super.viewWillAppear(animated) 29 | 30 | tableView.deselectRow() 31 | } 32 | 33 | override func didReceiveMemoryWarning() { 34 | super.didReceiveMemoryWarning() 35 | // Dispose of any resources that can be recreated. 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /v2ex/MemberCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MemberCell.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 7/1/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import v2exKit 11 | 12 | class MemberCell: UITableViewCell { 13 | 14 | @IBOutlet weak var avatarButton: UIButton! 15 | @IBOutlet weak var usernameButton: UIButton! 16 | 17 | override func awakeFromNib() { 18 | super.awakeFromNib() 19 | 20 | selectionStyle = UITableViewCellSelectionStyle.None 21 | avatarButton.layer.cornerRadius = 5 22 | avatarButton.layer.masksToBounds = true 23 | avatarButton.enabled = false 24 | usernameButton.contentHorizontalAlignment = UIControlContentHorizontalAlignment.Left 25 | usernameButton.setTitleColor(UIColor.colorWithHexString(kLinkColor), forState: .Disabled) 26 | usernameButton.enabled = false 27 | } 28 | 29 | func updateCell(member: MemberModel) -> Void { 30 | avatarButton.kf_setImageWithURL(NSURL(string: member.avatar_large)!, forState: .Disabled, placeholderImage: nil) 31 | usernameButton.setTitle(member.username, forState: .Disabled) 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /v2ex/MemberCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 26 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /v2ex/MemberModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MemberModel.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 5/8/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | private let dict = NSUserDefaults.standardUserDefaults().objectForKey(APIManage.domain) as? NSDictionary 13 | private let sharedInstance = dict==nil ? MemberModel(fromDictionary: ["id":0, "username":"", "avatar_large":""]) : MemberModel(fromDictionary: dict!) 14 | 15 | public class MemberModel: NSObject { 16 | public var username: String, avatar_large: String 17 | public var uid: Int?, website: String?, twitter: String?, psn: String?, github: String?, btc: String?, location: String?, tagline: String?, bio: String?, created: Int? 18 | 19 | public init(fromDictionary dictionary: NSDictionary) { 20 | 21 | self.username = dictionary["username"] as! String 22 | self.avatar_large = dictionary["avatar_large"] as! String 23 | 24 | if self.avatar_large.hasPrefix("//") { 25 | self.avatar_large = "http:"+self.avatar_large 26 | } 27 | 28 | self.uid = dictionary["id"] as? Int 29 | self.website = dictionary["website"] as? String 30 | self.twitter = dictionary["twitter"] as? String 31 | self.psn = dictionary["psn"] as? String 32 | self.github = dictionary["github"] as? String 33 | self.btc = dictionary["btc"] as? String 34 | self.location = dictionary["location"] as? String 35 | self.tagline = dictionary["tagline"] as? String 36 | self.bio = dictionary["bio"] as? String 37 | self.created = dictionary["created"] as? Int 38 | 39 | } 40 | 41 | public class var sharedMember: MemberModel { 42 | return sharedInstance 43 | } 44 | 45 | public func isLogin() -> Bool { 46 | return (uid != 0) && !username.isEmpty 47 | } 48 | 49 | public func saveUserData() { 50 | let defaults = NSUserDefaults.standardUserDefaults() 51 | defaults.setObject(["id":uid!, "username":username, "avatar_large":avatar_large], forKey: APIManage.domain) 52 | defaults.synchronize() 53 | } 54 | 55 | public func removeUserData() { 56 | self.uid = 0 57 | self.username = "" 58 | self.avatar_large = "" 59 | let defaults = NSUserDefaults.standardUserDefaults() 60 | defaults.removeObjectForKey(APIManage.domain) 61 | defaults.synchronize() 62 | // remove cookie 63 | let cookiesStorage = NSHTTPCookieStorage.sharedHTTPCookieStorage() 64 | if let cookies = cookiesStorage.cookiesForURL(NSURL(string: APIManage.baseURLString)!) { 65 | for cookie in cookies { 66 | cookiesStorage.deleteCookie(cookie) 67 | } 68 | } 69 | } 70 | 71 | public static func getUserInfo(account: AnyObject, completionHandler: (obj: MemberModel?, NSError?)->Void) { 72 | 73 | let args = (account is Int) ? ["id":account] : ["username":account] 74 | 75 | APIManage.sharedManager.request(.GET, APIManage.Router.ApiMember, parameters: args).responseJSON(options: .AllowFragments) { (response) -> Void in 76 | if response.result.isSuccess { 77 | let json = JSON(response.result.value!) 78 | let status = json["status"] 79 | if status == "found" { 80 | let result = MemberModel(fromDictionary: json.dictionaryObject!) 81 | completionHandler(obj: result, nil) 82 | }else{ 83 | let err = NSError(domain: APIManage.domain, code: 201, userInfo: [NSLocalizedDescriptionKey:"用户未找到"]) 84 | completionHandler(obj: nil, err) 85 | } 86 | 87 | }else{ 88 | let err = NSError(domain: APIManage.domain, code: 202, userInfo: [NSLocalizedDescriptionKey:"用户获取失败"]) 89 | completionHandler(obj: nil, err) 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /v2ex/MemberReplyCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MemberReplyCell.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 7/13/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import TTTAttributedLabel 11 | 12 | class MemberReplyCell: UITableViewCell { 13 | 14 | @IBOutlet weak var postTitleLabel: UILabel! 15 | @IBOutlet weak var replyContentLabel: UILabel! 16 | @IBOutlet weak var dateTimeLabel: UILabel! 17 | var markLayer: CAShapeLayer! 18 | 19 | override func setHighlighted(highlighted: Bool, animated: Bool) { 20 | super.setHighlighted(highlighted, animated: animated) 21 | 22 | cellSelected(highlighted, animated: animated) 23 | } 24 | 25 | override func setSelected(selected: Bool, animated: Bool) { 26 | super.setSelected(selected, animated: animated) 27 | 28 | cellSelected(selected, animated: animated) 29 | } 30 | 31 | override func awakeFromNib() { 32 | super.awakeFromNib() 33 | 34 | selectionStyle = .None 35 | layoutMargins = UIEdgeInsetsZero 36 | separatorInset = UIEdgeInsetsZero 37 | postTitleLabel.backgroundColor = UIColor.colorWithHexString("#edf3f5") 38 | postTitleLabel.textColor = UIColor.colorWithHexString("#778087") 39 | dateTimeLabel.backgroundColor = postTitleLabel.backgroundColor 40 | dateTimeLabel.textColor = UIColor.grayColor() 41 | 42 | let x = CGFloat(20), width = CGFloat(10), height = CGFloat(6) 43 | let path = UIBezierPath() 44 | path.moveToPoint(CGPoint(x: CGFloat(width/2), y: 0)) 45 | path.addLineToPoint(CGPoint(x: width, y: height)) 46 | path.addLineToPoint(CGPoint(x: 0, y: height)) 47 | path.closePath() 48 | 49 | self.markLayer = CAShapeLayer() 50 | markLayer.path = path.CGPath 51 | markLayer.fillColor = UIColor.whiteColor().CGColor 52 | markLayer.position = CGPoint(x: x, y: postTitleLabel.height-height) 53 | markLayer.actions = ["fillColor":NSNull()] 54 | contentView.layer.addSublayer(markLayer) 55 | } 56 | 57 | func updateCell(replyModel: MemberReplyModel) -> Void { 58 | postTitleLabel.text = " " + replyModel.post_title 59 | replyContentLabel.text = replyModel.reply_content 60 | dateTimeLabel.text = replyModel.date_time 61 | } 62 | 63 | func cellSelected(selected: Bool, animated: Bool) { 64 | markLayer.actions = animated ? nil : ["fillColor":NSNull()] 65 | if animated { 66 | UIView.animateWithDuration(0.25, animations: { () -> Void in 67 | self.changeColor(selected) 68 | }) 69 | } else { 70 | changeColor(selected) 71 | } 72 | } 73 | 74 | func changeColor(selected: Bool) { 75 | contentView.backgroundColor = selected ? UIColor.colorWithHexString("#d9d9d9") : UIColor.whiteColor() 76 | markLayer.fillColor = contentView.backgroundColor?.CGColor 77 | } 78 | 79 | } -------------------------------------------------------------------------------- /v2ex/MemberReplyModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MemberReplyModel.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 7/17/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | import SwiftyJSON 12 | import v2exKit 13 | import Kanna 14 | 15 | class MemberReplyModel: NSObject { 16 | 17 | var post_title: String, reply_content: String, date_time: String 18 | var post_id: Int 19 | 20 | init(fromDictionary dictionary: NSDictionary) { 21 | self.post_id = dictionary["post_id"] as! Int 22 | self.post_title = dictionary["post_title"] as! String 23 | self.reply_content = dictionary["reply_content"] as! String 24 | self.date_time = dictionary["date_time"] as! String 25 | } 26 | 27 | static func getMemberReplies(username: String, completionHandler:(obj: [MemberReplyModel], NSError?)->Void) { 28 | let url = APIManage.Router.Member + username + "/replies" 29 | var result = [MemberReplyModel]() 30 | let mgr = APIManage.sharedManager 31 | mgr.request(.GET, url, parameters: nil).responseString(encoding: nil, completionHandler: { (response) -> Void in 32 | 33 | if response.result.isSuccess { 34 | result = self.getMemberRepliesFromHtmlResponse(response.result.value!) 35 | completionHandler(obj: result, nil) 36 | } else { 37 | let err = NSError(domain: APIManage.domain, code: 202, userInfo: [NSLocalizedDescriptionKey:"数据获取失败"]) 38 | completionHandler(obj: [], err) 39 | } 40 | }) 41 | } 42 | 43 | /** 44 | 解析html 返回评论 45 | 46 | :param: respStr html string 47 | 48 | :returns: post 数组 49 | */ 50 | private static func getMemberRepliesFromHtmlResponse(respStr: String) -> [MemberReplyModel] { 51 | var result = [MemberReplyModel]() 52 | 53 | guard let doc = HTML(html: respStr, encoding: NSUTF8StringEncoding) else { 54 | return result 55 | } 56 | 57 | let body = doc.body! 58 | let divs = body.css("div[class='dock_area']") 59 | if divs.count > 0 { 60 | // 读取评论内容 61 | var contents = [String]() 62 | for contentNode in body.css("div[class='reply_content']") { 63 | let rawContent = contentNode.text! 64 | let content = htmlRegularExpression.stringByReplacingMatchesInString(rawContent, options: .Anchored, range: NSMakeRange(0, rawContent.characters.count), withTemplate: "") 65 | contents.append(content) 66 | } 67 | 68 | for (index, value) in divs.enumerate() { 69 | var postId = 0, postTitle = "", dateTime = "" 70 | if let postNode = value.at_css("span[class='gray']"), aNode = postNode.at_css("a") { 71 | postTitle = aNode.text! 72 | 73 | let href = aNode["href"]! 74 | let components = href.componentsSeparatedByString("/") 75 | if let componentsId = components.last?.componentsSeparatedByString("#") { 76 | if let first = componentsId.first { 77 | postId = (first as NSString).integerValue 78 | } 79 | } 80 | } 81 | if let timeNode = value.at_css("span[class='fade']"), timeText = timeNode.text { 82 | dateTime = timeText 83 | } 84 | // 评论内容 85 | let content = contents[index] 86 | 87 | let reply = ["post_id":postId, "post_title":postTitle, "reply_content":content, "date_time":dateTime] 88 | let replyModel = MemberReplyModel(fromDictionary: reply) 89 | result.append(replyModel) 90 | } 91 | 92 | } 93 | return result 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /v2ex/MemberReplyViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MemberReplyViewController.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 7/13/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import v2exKit 11 | 12 | class MemberReplyViewController: UITableViewController { 13 | 14 | var username: String! 15 | var dataSouce: [MemberReplyModel] = [] { 16 | didSet { 17 | tableView.reloadData() 18 | } 19 | } 20 | 21 | //args: NSDictionary 22 | func allocWithRouterParams(args: NSDictionary?) -> MemberReplyViewController { 23 | 24 | let viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("memberReplyViewController") as! MemberReplyViewController 25 | viewController.hidesBottomBarWhenPushed = true 26 | 27 | return viewController 28 | } 29 | 30 | override func viewDidLoad() { 31 | super.viewDidLoad() 32 | // Do any additional setup after loading the view, typically from a nib. 33 | 34 | tableView.layoutMargins = UIEdgeInsetsZero 35 | tableView.estimatedRowHeight = 60 36 | tableView.rowHeight = UITableViewAutomaticDimension 37 | tableView.tableFooterView = defaultTableFooterView 38 | 39 | reloadTableViewData(isPull: false) 40 | } 41 | 42 | override func viewWillAppear(animated: Bool) { 43 | super.viewWillAppear(animated) 44 | } 45 | 46 | override func didReceiveMemoryWarning() { 47 | super.didReceiveMemoryWarning() 48 | // Dispose of any resources that can be recreated. 49 | } 50 | 51 | func reloadTableViewData(isPull pull: Bool) { 52 | MemberReplyModel.getMemberReplies(username, completionHandler: { (obj, error) -> Void in 53 | if error == nil { 54 | self.dataSouce = obj 55 | } else { 56 | 57 | } 58 | }) 59 | } 60 | } 61 | 62 | // MARK: UITableViewDataSource & UITableViewDelegate 63 | extension MemberReplyViewController { 64 | // UITableViewDataSource 65 | override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 66 | let cell: MemberReplyCell = tableView.dequeueReusableCellWithIdentifier("memberReplyCellId") as! MemberReplyCell 67 | let replyModel = dataSouce[indexPath.row] 68 | cell.updateCell(replyModel) 69 | 70 | return cell 71 | } 72 | override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 73 | return dataSouce.count; 74 | } 75 | // UITableViewDelegate 76 | override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { 77 | let replyModel = dataSouce[indexPath.row] 78 | let viewController = PostDetailViewController().allocWithRouterParams(nil) 79 | viewController.postId = replyModel.post_id 80 | navigationController?.pushViewController(viewController, animated: true) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /v2ex/NodeModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NodeModel.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 5/8/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Kanna 11 | 12 | class NodeModel: NSObject { 13 | var name: String, title: String 14 | var node_id: Int?, header: String?, url: String?, topics: Int?, avatar_large: String? 15 | 16 | init(fromDictionary dictionary: NSDictionary) { 17 | self.node_id = dictionary["id"] as? Int 18 | self.name = dictionary["name"] as! String 19 | self.title = dictionary["title"] as! String 20 | self.url = dictionary["url"] as? String 21 | self.topics = dictionary["topics"] as? Int 22 | self.avatar_large = dictionary["avatar_large"] as? String 23 | 24 | if let header = dictionary["header"] as? String { 25 | self.header = header 26 | } 27 | } 28 | 29 | static func getNodeList(completionHandler:(obj: [AnyObject]?, NSError?)->Void) { 30 | 31 | let url = APIManage.baseURLString 32 | 33 | var result = [AnyObject]() 34 | 35 | let mgr = APIManage.sharedManager 36 | mgr.request(.GET, url, parameters: nil).responseString(encoding: nil, completionHandler: { (response) -> Void in 37 | 38 | if response.result.isSuccess { 39 | guard let doc = HTML(html: response.result.value!, encoding: NSUTF8StringEncoding) else { 40 | completionHandler(obj: result, nil) 41 | return 42 | } 43 | 44 | let body = doc.body! 45 | 46 | // parsing navi 47 | var data = ["title":"导航", "node":[], "type":NSNumber(integer: 2)] 48 | var nodeArr = [NodeModel]() 49 | for aNode in body.css("a[class='tab']") { 50 | let title = aNode.text! 51 | let href = aNode["href"]!.componentsSeparatedByString("=") 52 | let name = href.last! 53 | 54 | let nodeInfo = ["name":name, "title":title] as NSDictionary 55 | let nodeModel = NodeModel(fromDictionary: nodeInfo) 56 | nodeArr.append(nodeModel) 57 | } 58 | data["node"] = nodeArr 59 | if nodeArr.count > 0 { 60 | result.append(data) 61 | } 62 | 63 | // parsing node 64 | var titleArr = [String]() 65 | for divNode in body.css("div[class='cell']") { 66 | if let table = divNode.at_css("table"), tdFirst = table.css("td").first, span = tdFirst.at_css("span[class='fade']") { 67 | let a = table.css("td").last!.css("a") 68 | if a.count > 0 { 69 | var canAdd = true 70 | for titleStr in titleArr { 71 | if titleStr == span.text { 72 | canAdd = false 73 | } 74 | } 75 | if canAdd { 76 | titleArr.append(span.text!) 77 | var data = ["title":span.text!, "node":[], "type":NSNumber(integer: 1)] 78 | var nodeArr = [NodeModel]() 79 | for aNode in a { 80 | let title = aNode.text! 81 | let href = aNode["href"]!.componentsSeparatedByString("/") 82 | let name = href.last! 83 | let nodeInfo = ["name":name, "title":title] as NSDictionary 84 | 85 | let nodeModel = NodeModel(fromDictionary: nodeInfo) 86 | nodeArr.append(nodeModel) 87 | } 88 | data["node"] = nodeArr 89 | result.append(data) 90 | } 91 | } 92 | } 93 | } 94 | } 95 | completionHandler(obj: result, nil) 96 | 97 | }) 98 | 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /v2ex/NodeViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NodeViewController.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 5/8/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import v2exKit 11 | 12 | class NodeViewController: BaseViewController{ 13 | 14 | 15 | @IBOutlet weak var tableView: UITableView! 16 | 17 | var dataSouce: [AnyObject]! { 18 | didSet { 19 | self.tableView.reloadData() 20 | } 21 | } 22 | var indexPath: NSIndexPath! 23 | var refreshControl: UIRefreshControl! 24 | 25 | override func viewDidLoad() { 26 | super.viewDidLoad() 27 | // Do any additional setup after loading the view, typically from a nib. 28 | self.title = "节点" 29 | 30 | self.dataSouce = [] 31 | tableView.dataSource = self 32 | tableView.delegate = self 33 | tableView.rowHeight = 48 34 | tableView.tableFooterView = defaultTableFooterView 35 | 36 | self.refreshControl = UIRefreshControl(frame: self.tableView.bounds) 37 | refreshControl.addTarget(self, action: #selector(refresh), forControlEvents: UIControlEvents.ValueChanged) 38 | tableView.addSubview(self.refreshControl) 39 | 40 | reloadTableViewData(isPull: false) 41 | 42 | NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(userStatusChanged(_:)), name: v2exUserLogoutSuccessNotification, object: nil) 43 | NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(userStatusChanged(_:)), name: v2exUserLoginSuccessNotification, object: nil) 44 | } 45 | 46 | override func didReceiveMemoryWarning() { 47 | super.didReceiveMemoryWarning() 48 | // Dispose of any resources that can be recreated. 49 | } 50 | 51 | override func viewWillAppear(animated: Bool) { 52 | super.viewWillAppear(animated) 53 | 54 | if indexPath != nil { 55 | tableView.deselectRowAtIndexPath(indexPath, animated: true) 56 | } 57 | } 58 | 59 | deinit { 60 | NSNotificationCenter.defaultCenter().removeObserver(self, name: v2exUserLogoutSuccessNotification, object: nil) 61 | NSNotificationCenter.defaultCenter().removeObserver(self, name: v2exUserLoginSuccessNotification, object: nil) 62 | } 63 | 64 | func refresh() { 65 | self.reloadTableViewData(isPull: true) 66 | } 67 | 68 | func reloadTableViewData(isPull pull: Bool) { 69 | 70 | NodeModel.getNodeList({ (obj, error) -> Void in 71 | if error == nil { 72 | self.dataSouce = obj 73 | } 74 | 75 | if pull { 76 | self.refreshControl.endRefreshing() 77 | } 78 | }) 79 | } 80 | 81 | /// 82 | func getNodesBySection(section: Int) -> [NodeModel] { 83 | return dataSouce[section]["node"] as! [NodeModel] 84 | } 85 | 86 | // MARK: NSNotification 87 | 88 | func userStatusChanged(notification: NSNotification) { 89 | reloadTableViewData(isPull: false) 90 | } 91 | 92 | } 93 | 94 | // MARK: UITableViewDataSource & UITableViewDelegate 95 | extension NodeViewController: UITableViewDelegate, UITableViewDataSource { 96 | // UITableViewDataSource 97 | func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 98 | let cell: UITableViewCell = tableView.dequeueReusableCellWithIdentifier("nodeCellId")! 99 | cell.accessoryType = UITableViewCellAccessoryType.DisclosureIndicator 100 | 101 | let node = getNodesBySection(indexPath.section)[indexPath.row] 102 | cell.textLabel?.text = node.title 103 | 104 | return cell 105 | } 106 | 107 | func numberOfSectionsInTableView(tableView: UITableView) -> Int { 108 | return dataSouce.count 109 | } 110 | 111 | func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 112 | return getNodesBySection(section).count 113 | } 114 | 115 | func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 116 | return dataSouce[section]["title"] as? String 117 | } 118 | 119 | // UITableViewDelegate 120 | 121 | func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { 122 | self.indexPath = indexPath 123 | 124 | let node = getNodesBySection(indexPath.section)[indexPath.row] 125 | let type = dataSouce[indexPath.section]["type"] as! NSNumber 126 | 127 | let postViewController = PostViewController().allocWithRouterParams(nil) 128 | postViewController.title = node.title 129 | postViewController.dataType = PostType(rawValue: type.integerValue) 130 | postViewController.target = node.name 131 | self.navigationController?.pushViewController(postViewController, animated: true) 132 | } 133 | 134 | func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 135 | return 30 136 | } 137 | 138 | } -------------------------------------------------------------------------------- /v2ex/NotificationCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationCell.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 8/19/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class NotificationCell: UITableViewCell { 12 | 13 | @IBOutlet weak var postTitleLabel: UILabel! 14 | @IBOutlet weak var contentLabel: UILabel! 15 | @IBOutlet weak var dateTimeLabel: UILabel! 16 | var markLayer: CAShapeLayer! 17 | 18 | override func setHighlighted(highlighted: Bool, animated: Bool) { 19 | super.setHighlighted(highlighted, animated: animated) 20 | 21 | cellSelected(highlighted, animated: animated) 22 | } 23 | 24 | override func setSelected(selected: Bool, animated: Bool) { 25 | super.setSelected(selected, animated: animated) 26 | 27 | cellSelected(selected, animated: animated) 28 | } 29 | 30 | override func awakeFromNib() { 31 | super.awakeFromNib() 32 | 33 | selectionStyle = .None 34 | layoutMargins = UIEdgeInsetsZero 35 | separatorInset = UIEdgeInsetsZero 36 | postTitleLabel.backgroundColor = UIColor.colorWithHexString("#edf3f5") 37 | postTitleLabel.textColor = UIColor.colorWithHexString("#778087") 38 | dateTimeLabel.backgroundColor = postTitleLabel.backgroundColor 39 | dateTimeLabel.textColor = UIColor.grayColor() 40 | 41 | let x = CGFloat(20), width = CGFloat(10), height = CGFloat(6), y = postTitleLabel.height 42 | let path = UIBezierPath() 43 | path.moveToPoint(CGPointZero) 44 | path.addLineToPoint(CGPoint(x: width, y: 0)) 45 | path.addLineToPoint(CGPoint(x: CGFloat(width/2), y: height)) 46 | path.closePath() 47 | 48 | self.markLayer = CAShapeLayer() 49 | markLayer.path = path.CGPath 50 | markLayer.fillColor = postTitleLabel.backgroundColor!.CGColor 51 | markLayer.position = CGPoint(x: x, y: y) 52 | markLayer.actions = ["fillColor":NSNull()] 53 | contentView.layer.addSublayer(markLayer) 54 | } 55 | 56 | func updateCell(dataModel: NotificationModel) -> Void { 57 | contentLabel.text = dataModel.content 58 | dateTimeLabel.text = dataModel.smart_time 59 | let title = " " + dataModel.member.username + " 在 " + dataModel.title + " 中提到了你" as NSString 60 | let attributedStr = NSMutableAttributedString(string: title as String) 61 | let range = title.rangeOfString(dataModel.member.username) 62 | attributedStr.addAttribute(NSFontAttributeName, value: UIFont.boldSystemFontOfSize(12), range: range) 63 | postTitleLabel.attributedText = attributedStr 64 | } 65 | 66 | func cellSelected(selected: Bool, animated: Bool) { 67 | markLayer.actions = animated ? nil : ["fillColor":NSNull()] 68 | if animated { 69 | UIView.animateWithDuration(0.25, animations: { () -> Void in 70 | self.changeColor(selected) 71 | }) 72 | } else { 73 | changeColor(selected) 74 | } 75 | } 76 | 77 | func changeColor(selected: Bool) { 78 | contentView.backgroundColor = selected ? UIColor.colorWithHexString("#d9d9d9") : UIColor.whiteColor() 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /v2ex/NotificationManage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationManage.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 8/20/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import v2exKit 11 | import Kanna 12 | 13 | public class NotificationManage: NSObject { 14 | 15 | var notificationTimer: NSTimer! 16 | var hasNewNotification: Bool { 17 | return unreadCount > 0 18 | } 19 | var unreadCount = 0 { 20 | didSet { 21 | let tabBarViewController = UIApplication.sharedApplication().keyWindow?.rootViewController as! UITabBarController 22 | let viewController = tabBarViewController.viewControllers?.last as! UINavigationController 23 | viewController.tabBarItem.badgeValue = hasNewNotification ? "\(unreadCount)" : nil 24 | if let profileViewController = viewController.viewControllers.first as? ProfileViewController where profileViewController.isViewLoaded() { 25 | profileViewController.tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: 2, inSection: 1)], withRowAnimation: .Automatic) 26 | } 27 | UIApplication.sharedApplication().applicationIconBadgeNumber = unreadCount 28 | } 29 | } 30 | 31 | override init() { 32 | super.init() 33 | 34 | self.notificationTimer = NSTimer(timeInterval: 60, target: self, selector: #selector(timerHandler(_:)), userInfo: nil, repeats: true) 35 | NSRunLoop.mainRunLoop().addTimer(notificationTimer, forMode: NSRunLoopCommonModes) 36 | } 37 | 38 | internal static let sharedManager: NotificationManage = { 39 | return NotificationManage() 40 | }() 41 | 42 | // MARK: Timer 43 | func timerHandler(sender: NSTimer) { 44 | if !MemberModel.sharedMember.isLogin() { 45 | return 46 | } 47 | // println("timerHandler") 48 | let url = APIManage.baseURLString 49 | APIManage.sharedManager.request(.GET, url, parameters: nil).responseString(encoding: nil, completionHandler: { (response) -> Void in 50 | if response.result.isSuccess { 51 | guard let doc = HTML(html: response.result.value!, encoding: NSUTF8StringEncoding) else { 52 | return 53 | } 54 | if let notificationNode = doc.at_css("a[href='/notifications']"), docText = notificationNode.text { 55 | let components = docText.componentsSeparatedByString(" ") 56 | if components.first != nil { 57 | let unreadNum = (components.first! as NSString).integerValue //.toInt() 58 | self.unreadCount = unreadNum 59 | } 60 | } 61 | } 62 | }) 63 | } 64 | 65 | func timerStop() { 66 | notificationTimer.fireDate = NSDate.distantFuture() 67 | } 68 | 69 | func timerRestart() { 70 | print(notificationTimer) 71 | notificationTimer.fireDate = NSDate.distantPast() 72 | } 73 | } -------------------------------------------------------------------------------- /v2ex/NotificationModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationModel.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 8/10/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import v2exKit 11 | import Kanna 12 | 13 | let delFuncRegularExpression = try! NSRegularExpression(pattern: "[a-zA-z]+\\(([\\d]+),\\s*([\\d]+)\\)", options: NSRegularExpressionOptions.CaseInsensitive) 14 | 15 | class NotificationModel: NSObject { 16 | 17 | var content: String, title: String 18 | var n_id: Int, once_token: Int // 删除通知需要用到:$.post('/delete/notification/' + nId + '?once=' + token, function(data) { }) 19 | var post_id: Int 20 | var member: MemberModel 21 | var linkRange: [NSRange]?, linkMatched = false, contentRegexed = false, apiData = true 22 | var smart_time: String? 23 | 24 | init(fromDictionary dictionary: NSDictionary) { 25 | 26 | self.title = dictionary["title"] as! String 27 | self.content = dictionary["content"] as! String //!.stringValue 28 | self.n_id = dictionary["n_id"] as! Int 29 | self.once_token = dictionary["once_token"] as! Int 30 | self.post_id = dictionary["post_id"] as! Int 31 | 32 | self.smart_time = dictionary["smart_time"] as? String 33 | 34 | self.member = MemberModel(fromDictionary: dictionary["member"] as! NSDictionary) 35 | } 36 | 37 | /** 38 | 获取通知 39 | 40 | :param: completionHandler 41 | */ 42 | static func getUserNotifications(completionHandler:(obj: [NotificationModel], NSError?) -> Void) { 43 | let url = APIManage.Router.Notification 44 | var result = [NotificationModel]() 45 | let mgr = APIManage.sharedManager 46 | mgr.request(.GET, url, parameters: nil).responseString(encoding: nil, completionHandler: { (response) -> Void in 47 | 48 | if response.result.isSuccess { 49 | result = self.getPostsFromHtmlResponse(response.result.value!) 50 | completionHandler(obj: result, nil) 51 | } else { 52 | let err = NSError(domain: APIManage.domain, code: 202, userInfo: [NSLocalizedDescriptionKey:"数据获取失败"]) 53 | completionHandler(obj: [], err) 54 | } 55 | }) 56 | } 57 | 58 | /** 59 | 获取 60 | 61 | :param: respStr 返回的 html string 62 | 63 | :returns: post 数组 64 | */ 65 | static func getPostsFromHtmlResponse(respStr: String) -> [NotificationModel] { 66 | var result = [NotificationModel]() 67 | guard let doc = HTML(html: respStr, encoding: NSUTF8StringEncoding) else { 68 | return result 69 | } 70 | 71 | for oneNode in doc.body!.css("table") { 72 | var title = "", content = "", smart_time = "", username = "", avatar = "", n_id = 0, once_token = 0, post_id = 0 73 | if let payloadNode = oneNode.at_css("div[class='payload']"), rawContent = payloadNode.text { 74 | // content 75 | // let rawContent = payloadNode.text 76 | content = htmlRegularExpression.stringByReplacingMatchesInString(rawContent, options: [NSMatchingOptions.Anchored], range: NSMakeRange(0, rawContent.characters.count), withTemplate: "") 77 | // time 78 | if let timeNode = oneNode.at_css("span[class='snow']"), timeText = timeNode.text { 79 | smart_time = timeText 80 | } 81 | // username & title & post_id 82 | if let fadeNode = oneNode.at_css("span[class='fade']") { 83 | if let nameNode = fadeNode.at_css("strong"), usernameText = nameNode.text { 84 | username = usernameText 85 | } 86 | 87 | if let alast = fadeNode.css("a").last, titleText = alast.text, href = alast["href"] { 88 | title = titleText 89 | 90 | let components = href.componentsSeparatedByString("/") 91 | if let componentsId = components.last?.componentsSeparatedByString("#"), first = componentsId.first { 92 | post_id = (first as NSString).integerValue 93 | } 94 | } 95 | } 96 | // avatar 97 | if let avatarNode = oneNode.at_css("img[class='avatar']"), srcText = avatarNode["src"] { 98 | avatar = srcText 99 | } 100 | // n_id & once_token 101 | if let delNode = oneNode.at_css("a[class='node']"), onclickValue = delNode["onclick"] { 102 | let range = NSMakeRange(0, onclickValue.characters.count) 103 | let idString = delFuncRegularExpression.stringByReplacingMatchesInString(onclickValue, options: .Anchored, range: range, withTemplate: "$1-$2") 104 | let idArr = idString.componentsSeparatedByString("-") 105 | if let first = idArr.first { 106 | n_id = (first as NSString).integerValue 107 | } 108 | if let last = idArr.last { 109 | once_token = (last as NSString).integerValue 110 | } 111 | } 112 | 113 | let post = ["title":title, "content":content, "smart_time":smart_time, "n_id":n_id, "once_token":once_token, "post_id":post_id, "member":["username":username, "avatar_large":avatar]] as NSDictionary 114 | let model = NotificationModel(fromDictionary: post) 115 | result.append(model) 116 | } 117 | } 118 | return result 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /v2ex/NotificationViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationViewController.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 8/19/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import v2exKit 11 | 12 | class NotificationViewController: UITableViewController { 13 | 14 | var dataSouce: [NotificationModel] = [] { 15 | didSet { 16 | tableView.reloadData() 17 | } 18 | } 19 | 20 | //args: NSDictionary 21 | func allocWithRouterParams(args: NSDictionary?) -> NotificationViewController { 22 | 23 | let viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("notificationViewController") as! NotificationViewController 24 | viewController.hidesBottomBarWhenPushed = true 25 | 26 | return viewController 27 | } 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | // Do any additional setup after loading the view, typically from a nib. 32 | 33 | tableView.layoutMargins = UIEdgeInsetsZero 34 | tableView.estimatedRowHeight = 60 35 | tableView.rowHeight = UITableViewAutomaticDimension 36 | tableView.tableFooterView = defaultTableFooterView 37 | 38 | reloadTableViewData(isPull: false) 39 | } 40 | 41 | override func didReceiveMemoryWarning() { 42 | super.didReceiveMemoryWarning() 43 | // Dispose of any resources that can be recreated. 44 | } 45 | 46 | func reloadTableViewData(isPull pull: Bool) { 47 | NotificationModel.getUserNotifications { (obj, error) -> Void in 48 | if error == nil { 49 | self.dataSouce = obj 50 | } 51 | } 52 | } 53 | } 54 | 55 | // MARK: UITableViewDataSource & UITableViewDelegate 56 | extension NotificationViewController { 57 | // UITableViewDataSource 58 | override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 59 | let cell: NotificationCell = tableView.dequeueReusableCellWithIdentifier("notificationCellId") as! NotificationCell 60 | let dataModel = dataSouce[indexPath.row] 61 | cell.updateCell(dataModel) 62 | 63 | return cell 64 | } 65 | override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 66 | return dataSouce.count; 67 | } 68 | // UITableViewDelegate 69 | override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { 70 | let dataModel = dataSouce[indexPath.row] 71 | let viewController = PostDetailViewController().allocWithRouterParams(nil) 72 | viewController.postId = dataModel.post_id 73 | navigationController?.pushViewController(viewController, animated: true) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /v2ex/PostCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostCell.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 6/16/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import v2exKit 11 | import SnapKit 12 | import FontAwesome 13 | 14 | class PostCell: UITableViewCell { 15 | 16 | @IBOutlet weak var picView: UIImageView! 17 | @IBOutlet weak var titleLabel: UILabel! 18 | @IBOutlet weak var nodeLabel: UILabel! 19 | @IBOutlet weak var usernameLabel: UILabel! 20 | @IBOutlet weak var repliesLabel: UILabel! 21 | @IBOutlet weak var timeLabel: UILabel! 22 | @IBOutlet weak var picViewWidthConstraint: NSLayoutConstraint! 23 | 24 | override func awakeFromNib() { 25 | super.awakeFromNib() 26 | 27 | layoutMargins = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 0) 28 | separatorInset = UIEdgeInsetsZero 29 | picView.layer.cornerRadius = 5 30 | picView.layer.masksToBounds = true 31 | nodeLabel.textColor = UIColor.grayColor() 32 | usernameLabel.font = UIFont.fontAwesomeOfSize(12) 33 | timeLabel.textAlignment = NSTextAlignment.Center 34 | timeLabel.textColor = UIColor.grayColor() 35 | timeLabel.font = UIFont.fontAwesomeOfSize(12) 36 | usernameLabel.textColor = UIColor.grayColor() 37 | timeLabel.hidden = true 38 | repliesLabel.font = usernameLabel.font 39 | repliesLabel.textColor = usernameLabel.textColor 40 | titleLabel.font = kTitleFont 41 | } 42 | 43 | func updateCell(post: PostModel) -> Void { 44 | picViewWidthConstraint.constant = 40 45 | if post.member.avatar_large.isEmpty { 46 | picViewWidthConstraint.constant = 0.1 47 | picView.image = nil 48 | } else { 49 | picView.kf_setImageWithURL(NSURL(string: post.member.avatar_large)!, placeholderImage: nil) 50 | } 51 | titleLabel.text = post.title 52 | nodeLabel.text = "[\(post.node)]" 53 | usernameLabel.text = String.fontAwesomeIconWithName(.User)+" \(post.member.username)" 54 | repliesLabel.text = String.fontAwesomeIconWithName(.Comment)+" \(post.replies)" 55 | 56 | if post.node.isEmpty { 57 | nodeLabel.font = UIFont.fontAwesomeOfSize(12) 58 | nodeLabel.text = String.fontAwesomeIconWithName(.User)+" \(post.member.username)" 59 | repliesLabel.hidden = true 60 | usernameLabel.text = String.fontAwesomeIconWithName(.Comment)+" \(post.replies)" 61 | } 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /v2ex/PostCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 31 | 40 | 49 | 58 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /v2ex/PostContentCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostContentCell.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 7/2/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import v2exKit 11 | import TTTAttributedLabel 12 | 13 | class PostContentCell: UITableViewCell { 14 | 15 | @IBOutlet weak var titleLabel: UILabel! 16 | @IBOutlet weak var contentLabel: TTTAttributedLabel! 17 | @IBOutlet weak var timeLabel: UILabel! 18 | @IBOutlet weak var usernameButton: UIButton! 19 | @IBOutlet weak var avatarButton: UIButton! 20 | 21 | 22 | override func awakeFromNib() { 23 | super.awakeFromNib() 24 | 25 | layoutMargins = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 0) 26 | separatorInset = UIEdgeInsetsZero 27 | titleLabel.font = kTitleFont 28 | 29 | avatarButton.layer.cornerRadius = 5 30 | avatarButton.layer.masksToBounds = true 31 | 32 | timeLabel.font = UIFont.systemFontOfSize(12) 33 | timeLabel.textColor = UIColor.grayColor() 34 | 35 | contentLabel.font = UIFont.systemFontOfSize(14) 36 | var linkAttributes = Dictionary() 37 | linkAttributes[kCTForegroundColorAttributeName as String] = UIColor.colorWithHexString(kLinkColor).CGColor 38 | contentLabel.linkAttributes = linkAttributes 39 | contentLabel.extendsLinkTouchArea = false 40 | contentLabel.font = kContentFont 41 | } 42 | 43 | func updateCell(postDetail: PostDetailModel) -> Void { 44 | 45 | titleLabel.text = postDetail.title 46 | usernameButton.setTitle(postDetail.member.username, forState: UIControlState.Normal) 47 | usernameButton.setTitleColor(UIColor.colorWithHexString(kLinkColor), forState: .Normal) 48 | avatarButton.kf_setImageWithURL(NSURL(string: postDetail.member.avatar_large)!, forState: .Normal, placeholderImage: nil) 49 | timeLabel.text = postDetail.getSmartTime() 50 | 51 | 52 | var linkRange = [NSRange]() 53 | contentLabel.setText(postDetail.content, afterInheritingLabelAttributesAndConfiguringWithBlock: { (mutableAttributedString) -> NSMutableAttributedString! in 54 | 55 | let stringRange = NSMakeRange(0, mutableAttributedString.length) 56 | // username 57 | usernameRegularExpression.enumerateMatchesInString(mutableAttributedString.string, options: NSMatchingOptions.ReportCompletion, range: stringRange, usingBlock: { (result, flags, stop) -> Void in 58 | 59 | if let resultVal = result { 60 | addLinkAttributed(mutableAttributedString, range: resultVal.range) 61 | linkRange.append(resultVal.range) 62 | } 63 | }) 64 | // http link 65 | httpRegularExpression.enumerateMatchesInString(mutableAttributedString.string, options: NSMatchingOptions.ReportCompletion, range: stringRange, usingBlock: { (result, flags, stop) -> Void in 66 | 67 | if let resultVal = result { 68 | addLinkAttributed(mutableAttributedString, range: resultVal.range) 69 | linkRange.append(resultVal.range) 70 | } 71 | }) 72 | 73 | return mutableAttributedString 74 | }) 75 | 76 | if linkRange.count > 0 { 77 | for range in linkRange { 78 | let linkStr = (postDetail.content as NSString).substringWithRange(range) 79 | contentLabel.addLinkToURL(NSURL(string: linkStr), withRange: range) 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /v2ex/PostDetailModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostDetailModel.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 6/8/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | import SwiftyJSON 12 | import Kanna 13 | import CoreSpotlight 14 | import MobileCoreServices 15 | import v2exKit 16 | 17 | class PostDetailModel: NSObject { 18 | 19 | var aid: Int, replies: Int, created: Int, last_modified: Int, last_touched: Int 20 | var title: String, url: String, content: String, content_rendered: String 21 | var member: MemberModel, node: NodeModel 22 | var tags: [String]? 23 | 24 | init(fromDictionary dictionary: NSDictionary) { 25 | self.aid = dictionary["id"] as! Int 26 | self.url = dictionary["url"] as! String //!.stringValue 27 | self.content = dictionary["content"] as! String //!.stringValue 28 | self.title = dictionary["title"] as! String //!.stringValue 29 | self.created = dictionary["created"] as! Int 30 | self.last_modified = dictionary["last_modified"] as! Int 31 | self.last_touched = dictionary["last_touched"] as! Int 32 | self.replies = dictionary["replies"] as! Int 33 | self.content_rendered = dictionary["content_rendered"] as! String 34 | 35 | self.member = MemberModel(fromDictionary: dictionary["member"] as! NSDictionary) 36 | self.node = NodeModel(fromDictionary: dictionary["node"] as! NSDictionary) 37 | } 38 | 39 | func getSmartTime() -> String { 40 | return String.smartDate(Double(self.created)) 41 | } 42 | 43 | static func getPostDetail(postId:Int, completionHandler:(detail: PostDetailModel?, NSError?)->Void) { 44 | 45 | let url = APIManage.Router.ApiTopic + String(postId) 46 | 47 | Alamofire.request(.GET, url).responseJSON(options: .AllowFragments) { (response) -> Void in 48 | 49 | if response.result.isSuccess { 50 | let json = JSON(response.result.value!).arrayValue 51 | let first = json.first?.dictionaryObject 52 | let data = PostDetailModel(fromDictionary: first!) 53 | completionHandler(detail: data, nil) 54 | // 索引文章 55 | if #available(iOS 9.0, *) { 56 | PostDetailModel.getPostTags(postId, completionHandler: { (tags, error) -> Void in 57 | if let tagArr = tags { 58 | data.tags = tagArr 59 | PostDetailModel.indexPost(data) 60 | } 61 | }) 62 | } 63 | }else{ 64 | let err = NSError(domain: APIManage.domain, code: 202, userInfo: [NSLocalizedDescriptionKey:"数据获取失败"]) 65 | completionHandler(detail: nil, err) 66 | } 67 | 68 | } 69 | } 70 | 71 | static func getPostTags(postId: Int, completionHandler:(tags: [String]?, NSError?)->Void) { 72 | // 使用 APIManage 获取的数据中没有包含 tag 的数据,暂时不知道原因 73 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in 74 | do { 75 | let postURL = APIManage.Router.Post + "\(postId)" 76 | let html = try String(contentsOfURL: NSURL(string: postURL)!, encoding: NSUTF8StringEncoding) 77 | guard let doc = HTML(html: html, encoding: NSUTF8StringEncoding) else { 78 | return 79 | } 80 | var tagArr = [String]() 81 | for aNode in doc.body!.css("a[class='tag']") { 82 | if let text = aNode.text { 83 | tagArr.append(text.trim()) 84 | } 85 | } 86 | dispatch_async(dispatch_get_main_queue(), { () -> Void in 87 | completionHandler(tags: tagArr, nil) 88 | }) 89 | } catch { 90 | dispatch_async(dispatch_get_main_queue(), { () -> Void in 91 | let err = NSError(domain: APIManage.domain, code: 202, userInfo: [NSLocalizedDescriptionKey:"数据获取失败"]) 92 | completionHandler(tags: nil, err) 93 | }) 94 | } 95 | } 96 | } 97 | 98 | @available(iOS 9.0, *) 99 | static func indexPost(post: PostDetailModel) { 100 | var content = post.content 101 | if content.characters.count > 40 { 102 | content = content.substringToIndex(content.startIndex.advancedBy(40)) 103 | } 104 | let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeImage as String) 105 | attributeSet.title = post.title 106 | attributeSet.contentDescription = content 107 | attributeSet.keywords = post.tags! 108 | let searchableItem = CSSearchableItem(uniqueIdentifier: String(format: kAppPostScheme, arguments: [post.aid]), domainIdentifier: "cc.shitoudev.v2ex.post", attributeSet: attributeSet) 109 | CSSearchableIndex.defaultSearchableIndex().indexSearchableItems([searchableItem]) { (error) -> Void in 110 | if error != nil { 111 | print("failed with error:\(error)\n") 112 | } else { 113 | print("Indexed!\n") 114 | } 115 | } 116 | } 117 | 118 | } -------------------------------------------------------------------------------- /v2ex/PostTableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostTableView.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 6/5/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import v2exKit 11 | 12 | class PostTableView: UITableView { 13 | 14 | var dataType: PostType! 15 | var target: String! { 16 | didSet { 17 | self.reloadTableViewData(isPull: false) 18 | } 19 | } 20 | 21 | var dataSouce: [PostModel] = [] { 22 | didSet { 23 | self.reloadData() 24 | } 25 | } 26 | var indexPath: NSIndexPath! 27 | var refreshControl: UIRefreshControl! 28 | 29 | override init(frame: CGRect, style: UITableViewStyle) { 30 | super.init(frame: frame, style: style) 31 | 32 | dataSource = self 33 | delegate = self 34 | rowHeight = 56 35 | layoutMargins = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 0) 36 | registerNib(UINib(nibName: "PostCell", bundle: nil), forCellReuseIdentifier: "postCellId") 37 | tableFooterView = defaultTableFooterView 38 | 39 | self.refreshControl = UIRefreshControl(frame: self.bounds) 40 | refreshControl.addTarget(self, action: #selector(refresh), forControlEvents: UIControlEvents.ValueChanged) 41 | self.addSubview(self.refreshControl) 42 | } 43 | 44 | required init(coder aDecoder: NSCoder) { 45 | fatalError("init(coder:) has not been implemented") 46 | } 47 | 48 | func refresh() { 49 | self.reloadTableViewData(isPull: true) 50 | } 51 | 52 | func reloadTableViewData(isPull pull: Bool) { 53 | PostModel.getPostList(self.dataType, target: self.target, completionHandler: { (obj, error) -> Void in 54 | if error == nil { 55 | self.dataSouce = obj 56 | } else { 57 | 58 | } 59 | 60 | if pull { 61 | self.refreshControl.endRefreshing() 62 | } 63 | }) 64 | } 65 | 66 | func deselectRow() -> Void { 67 | if (indexPath != nil) { 68 | deselectRowAtIndexPath(indexPath, animated: true) 69 | } 70 | } 71 | 72 | } 73 | 74 | // MARK: UITableViewDataSource & UITableViewDelegate 75 | extension PostTableView: UITableViewDelegate, UITableViewDataSource { 76 | // UITableViewDataSource 77 | func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 78 | let cell: PostCell = tableView.dequeueReusableCellWithIdentifier("postCellId") as! PostCell 79 | 80 | let post = dataSouce[indexPath.row] 81 | cell.updateCell(post) 82 | 83 | return cell 84 | } 85 | 86 | func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 87 | return dataSouce.count; 88 | } 89 | 90 | // UITableViewDelegate 91 | func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { 92 | self.indexPath = indexPath 93 | if let vc = self.traverseResponderChainForUIViewController() where vc.isKindOfClass(UIViewController) { 94 | let post = self.dataSouce[indexPath.row] 95 | let viewController = PostDetailViewController().allocWithRouterParams(nil) 96 | viewController.postId = post.postId 97 | vc.navigationController?.pushViewController(viewController, animated: true) 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /v2ex/PostViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostViewController.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 6/16/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import v2exKit 11 | 12 | class PostViewController: BaseViewController { 13 | 14 | var tableView: PostTableView! 15 | 16 | var dataType: PostType! 17 | var target: String! 18 | 19 | //args: NSDictionary 20 | func allocWithRouterParams(args: NSDictionary?) -> PostViewController { 21 | 22 | let viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("postViewController") as! PostViewController 23 | viewController.hidesBottomBarWhenPushed = true 24 | 25 | return viewController 26 | } 27 | 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | // Do any additional setup after loading the view, typically from a nib. 31 | 32 | self.tableView = PostTableView(frame: view.bounds, style: .Plain) 33 | tableView.dataType = dataType 34 | tableView.target = target 35 | self.view = tableView 36 | } 37 | 38 | override func viewWillAppear(animated: Bool) { 39 | super.viewWillAppear(animated) 40 | 41 | tableView.deselectRow() 42 | } 43 | 44 | override func didReceiveMemoryWarning() { 45 | super.didReceiveMemoryWarning() 46 | // Dispose of any resources that can be recreated. 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /v2ex/Vendor/DAKeyboardControl/DAKeyboardControl.h: -------------------------------------------------------------------------------- 1 | // 2 | // DAKeyboardControl.h 3 | // DAKeyboardControlExample 4 | // 5 | // Created by Daniel Amitay on 7/14/12. 6 | // Copyright (c) 2012 Daniel Amitay. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | typedef void (^DAKeyboardDidMoveBlock)(CGRect keyboardFrameInView, BOOL opening, BOOL closing); 12 | 13 | /** DAKeyboardControl allows you to easily add keyboard awareness and scrolling 14 | dismissal (a receding keyboard ala iMessages app) to any UIView, UIScrollView 15 | or UITableView with only 1 line of code. DAKeyboardControl automatically 16 | extends UIView and provides a block callback with the keyboard's current origin. 17 | */ 18 | 19 | @interface UIView (DAKeyboardControl) 20 | 21 | /** The keyboardTriggerOffset property allows you to choose at what point the 22 | user's finger "engages" the keyboard. 23 | */ 24 | @property (nonatomic) CGFloat keyboardTriggerOffset; 25 | @property (nonatomic, readonly) BOOL keyboardWillRecede; 26 | 27 | /** Adding pan-to-dismiss (functionality introduced in iMessages) 28 | @param didMoveBlock called everytime the keyboard is moved so you can update 29 | the frames of your views 30 | @see addKeyboardNonpanningWithActionHandler: 31 | @see removeKeyboardControl 32 | */ 33 | - (void)addKeyboardPanningWithActionHandler:(DAKeyboardDidMoveBlock)didMoveBlock; 34 | - (void)addKeyboardPanningWithFrameBasedActionHandler:(DAKeyboardDidMoveBlock)didMoveFrameBasesBlock 35 | constraintBasedActionHandler:(DAKeyboardDidMoveBlock)didMoveConstraintBasesBlock; 36 | 37 | /** Adding keyboard awareness (appearance and disappearance only) 38 | @param didMoveBlock called everytime the keyboard is moved so you can update 39 | the frames of your views 40 | @see addKeyboardPanningWithActionHandler: 41 | @see removeKeyboardControl 42 | */ 43 | - (void)addKeyboardNonpanningWithActionHandler:(DAKeyboardDidMoveBlock)didMoveBlock; 44 | - (void)addKeyboardNonpanningWithFrameBasedActionHandler:(DAKeyboardDidMoveBlock)didMoveFrameBasesBlock 45 | constraintBasedActionHandler:(DAKeyboardDidMoveBlock)didMoveConstraintBasesBlock; 46 | 47 | /** Remove the keyboard action handler 48 | @note You MUST call this method to remove the keyboard handler before the view 49 | goes out of memory. 50 | */ 51 | - (void)removeKeyboardControl; 52 | 53 | /** Returns the keyboard frame in the view */ 54 | - (CGRect)keyboardFrameInView; 55 | @property (nonatomic, readonly, getter = isKeyboardOpened) BOOL keyboardOpened; 56 | 57 | /** Convenience method to dismiss the keyboard */ 58 | - (void)hideKeyboard; 59 | 60 | @end 61 | -------------------------------------------------------------------------------- /v2ex/Vendor/FlurrySDK/Flurry/Empty.m: -------------------------------------------------------------------------------- 1 | 2 | #import "Flurry.h" 3 | 4 | @implementation Flurry (ForceLoad) 5 | 6 | @end -------------------------------------------------------------------------------- /v2ex/Vendor/FlurrySDK/Flurry/FlurryWatch.h: -------------------------------------------------------------------------------- 1 | // 2 | // FlurryWatch.h 3 | // Flurry iOS Analytics Agent 4 | // 5 | // Copyright 2009-2015 Flurry, Inc. All rights reserved. 6 | // 7 | // Methods in this header file are for use with Flurry Analytics 8 | 9 | #import "Flurry.h" 10 | 11 | @interface FlurryWatch : NSObject 12 | 13 | /*! 14 | * @brief Records a custom event specified by @c eventName. 15 | * @since 6.4.0 16 | * 17 | * This method allows you to specify custom watch events within your watch extension. 18 | * As a general rule you should capture events related to user navigation within your 19 | * app, any actionaround monetization, and other events as they are applicable to 20 | * tracking progress towards your business goals. 21 | * 22 | * @note You should not pass private or confidential information about your users in a 23 | * custom event. \n 24 | * This method is only supported within a watch extension.\n 25 | * 26 | * @see #logWatchEvent:withParameters: for details on storing events with parameters. \n 27 | * 28 | * @code 29 | * - (void)interestingAppAction 30 | { 31 | [FlurryWatch logWatchEvent:@"Interesting_Action"]; 32 | // Perform interesting action 33 | } 34 | * @endcode 35 | * 36 | * @param eventName Name of the event. For maximum effectiveness, we recommend using a naming scheme 37 | * that can be easily understood by non-technical people in your business domain. 38 | * 39 | * @return enum FlurryEventRecordStatus for the recording status of the logged event. 40 | */ 41 | + (FlurryEventRecordStatus)logWatchEvent:(NSString *)eventName; 42 | 43 | /*! 44 | * @brief Records a custom parameterized event specified by @c eventName with @c parameters. 45 | * @since 6.4.0 46 | * 47 | * This method overloads #logWatchEvent to allow you to associate parameters with an event. Parameters 48 | * are extremely valuable as they allow you to store characteristics of an action. For example, 49 | * if a user clicked a confirmation button, it may be useful to know the reservation details. 50 | * By setting this parameter you will be able to view a distribution of reservations 51 | * on the Flurrly Dev Portal. 52 | * 53 | * @note You should not pass private or confidential information about your users in a 54 | * custom event. \n 55 | * A maximum of 10 parameter names may be associated with any event. Sending 56 | * over 10 parameter names with a single event will result in no parameters being logged 57 | * for that event. You may specify an infinite number of Parameter values. For example, 58 | * a Search Box would have 1 parameter name (e.g. - Search Box) and many values, which would 59 | * allow you to see what values users look for the most in your app. \n 60 | * Where applicable, you should make a concerted effort to use timed events with 61 | * parameters (#logEvent:withParameters:timed:). This provides valuable information 62 | * around the time the user spends within an action (e.g. - time spent on a level or 63 | * viewing a page). 64 | * This method is only supported within a watch extension.\n 65 | * 66 | * @code 67 | * - (void)userConfirmedTheReservation 68 | { 69 | NSDictionary *params = 70 | [NSDictionary dictionaryWithObjectsAndKeys:@"Great Restaurant", // Parameter Value 71 | @"Destination", // Parameter Name 72 | nil]; 73 | [FlurryWatch logWatchEvent:@"Reservation Confirmed" withParameters:params]; 74 | // Confirm the reservation 75 | } 76 | * @endcode 77 | * 78 | * @param eventName Name of the event. For maximum effectiveness, we recommend using a naming scheme 79 | * that can be easily understood by non-technical people in your business domain. 80 | * @param parameters An immutable copy of map containing Name-Value pairs of parameters. 81 | * 82 | * @return enum FlurryEventRecordStatus for the recording status of the logged event. 83 | */ 84 | + (FlurryEventRecordStatus)logWatchEvent:(NSString *)eventName withParameters:(NSDictionary *)parameters; 85 | 86 | /*! 87 | * @brief Records a watch exception. Commonly used to catch unhandled exceptions. 88 | * @since 6.4.0 89 | * 90 | * This method captures an exception for reporting to Flurry. We recommend adding an uncaught 91 | * exception listener to capture any exceptions that occur during usage that is not 92 | * anticipated by your app. 93 | * 94 | * @note This method is only supported within a watch extension.\n 95 | * 96 | * @see #logWatchError:message:error: for details on capturing errors. 97 | * 98 | * @code 99 | * - (void) uncaughtExceptionHandler(NSException *exception) 100 | { 101 | [FlurryWatch logWatchError:@"Uncaught" message:@"Crash!" exception:exception]; 102 | } 103 | * @endcode 104 | * 105 | * @param errorID Name of the error. 106 | * @param message The message to associate with the error. 107 | * @param exception The exception object to report. 108 | */ 109 | + (void)logWatchError:(NSString *)errorID message:(NSString *)message exception:(NSException *)exception; 110 | 111 | /*! 112 | * @brief Records a watch error. 113 | * @since 6.4.0 114 | * 115 | * This method captures an error for reporting to Flurry. 116 | * 117 | * @note This method is only supported within a watch extension.\n 118 | * 119 | * @see #logWatchError:message:exception: for details on capturing exceptions. 120 | * 121 | * @code 122 | * - (void) webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error 123 | { 124 | [FlurryWatch logWatchError:@"Interface failed to load" message:[error localizedDescription] error:error]; 125 | } 126 | * @endcode 127 | * 128 | * @param errorID Name of the error. 129 | * @param message The message to associate with the error. 130 | * @param error The error object to report. 131 | */ 132 | + (void)logWatchError:(NSString *)errorID message:(NSString *)message error:(NSError *)error; 133 | 134 | @end 135 | -------------------------------------------------------------------------------- /v2ex/Vendor/FlurrySDK/Flurry/libFlurry_7.3.0.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shitoudev/v2ex/12ba9eb9b614c6506cb67e1a9f0745bf0611c0cf/v2ex/Vendor/FlurrySDK/Flurry/libFlurry_7.3.0.a -------------------------------------------------------------------------------- /v2ex/Vendor/JDStatusBarNotification/JDStatusBarNotification.h: -------------------------------------------------------------------------------- 1 | // 2 | // JDStatusBarNotification.h 3 | // 4 | // Based on KGStatusBar by Kevin Gibbon 5 | // 6 | // Created by Markus Emrich on 10/28/13. 7 | // Copyright 2013 Markus Emrich. All rights reserved. 8 | // 9 | 10 | #import 11 | 12 | #import "JDStatusBarStyle.h" 13 | #import "JDStatusBarView.h" 14 | 15 | /** 16 | * A block that is used to define the appearance of a notification. 17 | * A JDStatusBarStyle instance defines the notification appeareance. 18 | * 19 | * @param style The current default JDStatusBarStyle instance. 20 | * 21 | * @return The modified JDStatusBarStyle instance. 22 | */ 23 | typedef JDStatusBarStyle*(^JDPrepareStyleBlock)(JDStatusBarStyle *style); 24 | 25 | /** 26 | * This class is a singletion which is used to present notifications 27 | * on top of the status bar. To present a notification, use one of the 28 | * given class methods. 29 | */ 30 | @interface JDStatusBarNotification : NSObject 31 | 32 | #pragma mark Presentation 33 | 34 | /** 35 | * Show a notification. It won't hide automatically, 36 | * you have to dimiss it on your own. 37 | * 38 | * @param status The message to display 39 | * 40 | * @return The presented notification view for further customization 41 | */ 42 | + (JDStatusBarView*)showWithStatus:(NSString *)status; 43 | 44 | /** 45 | * Show a notification with a specific style. It won't 46 | * hide automatically, you have to dimiss it on your own. 47 | * 48 | * @param status The message to display 49 | * @param styleName The name of the style. You can use any JDStatusBarStyle constant 50 | * (JDStatusBarStyleDefault, etc.), or a custom style identifier, after you added a 51 | * custom style. If this is nil, the default style will be used. 52 | * 53 | * @return The presented notification view for further customization 54 | */ 55 | + (JDStatusBarView*)showWithStatus:(NSString *)status 56 | styleName:(NSString*)styleName; 57 | 58 | /** 59 | * Same as showWithStatus:, but the notification will 60 | * automatically dismiss after the given timeInterval. 61 | * 62 | * @param status The message to display 63 | * @param timeInterval The duration, how long the notification 64 | * is displayed. (Including the animation duration) 65 | * 66 | * @return The presented notification view for further customization 67 | */ 68 | + (JDStatusBarView*)showWithStatus:(NSString *)status 69 | dismissAfter:(NSTimeInterval)timeInterval; 70 | 71 | /** 72 | * Same as showWithStatus:styleName:, but the notification 73 | * will automatically dismiss after the given timeInterval. 74 | * 75 | * @param status The message to display 76 | * @param timeInterval The duration, how long the notification 77 | * is displayed. (Including the animation duration) 78 | * @param styleName The name of the style. You can use any JDStatusBarStyle constant 79 | * (JDStatusBarStyleDefault, etc.), or a custom style identifier, after you added a 80 | * custom style. If this is nil, the default style will be used. 81 | * 82 | * @return The presented notification view for further customization 83 | */ 84 | + (JDStatusBarView*)showWithStatus:(NSString *)status 85 | dismissAfter:(NSTimeInterval)timeInterval 86 | styleName:(NSString*)styleName; 87 | 88 | #pragma mark Dismissal 89 | 90 | /** 91 | * Calls dismissAnimated: with animated set to YES 92 | */ 93 | + (void)dismiss; 94 | 95 | /** 96 | * Dismisses any currently displayed notification immediately 97 | * 98 | * @param animated If this is YES, the animation style used 99 | * for presentation will also be used for the dismissal. 100 | */ 101 | + (void)dismissAnimated:(BOOL)animated; 102 | 103 | /** 104 | * Same as dismissAnimated:, but you can specify a delay, 105 | * so the notification wont be dismissed immediately 106 | * 107 | * @param delay The delay, how long the notification should stay visible 108 | */ 109 | + (void)dismissAfter:(NSTimeInterval)delay; 110 | 111 | #pragma mark Styles 112 | 113 | /** 114 | * This changes the default style, which is always used 115 | * when a method without styleName is used for presentation, or 116 | * styleName is nil, or no style is found with this name. 117 | * 118 | * @param prepareBlock A block, which has a JDStatusBarStyle instance as 119 | * parameter. This instance can be modified to suit your needs. You need 120 | * to return the modified style again. 121 | */ 122 | + (void)setDefaultStyle:(JDPrepareStyleBlock)prepareBlock; 123 | 124 | /** 125 | * Adds a custom style, which than can be used 126 | * in the presentation methods. 127 | * 128 | * @param identifier The identifier, which will 129 | * later be used to reference the configured style. 130 | * @param prepareBlock A block, which has a JDStatusBarStyle instance as 131 | * parameter. This instance can be modified to suit your needs. You need 132 | * to return the modified style again. 133 | * 134 | * @return Returns the given identifier, so it can 135 | * be directly used as styleName parameter. 136 | */ 137 | + (NSString*)addStyleNamed:(NSString*)identifier 138 | prepare:(JDPrepareStyleBlock)prepareBlock; 139 | 140 | #pragma mark progress & activity 141 | 142 | /** 143 | * Show the progress below the label. 144 | * 145 | * @param progress Relative progress from 0.0 to 1.0 146 | */ 147 | + (void)showProgress:(CGFloat)progress; 148 | 149 | /** 150 | * Shows an activity indicator in front of the notification text 151 | * 152 | * @param show Use this flag to show or hide the activity indicator 153 | * @param style Sets the style of the activity indicator 154 | */ 155 | + (void)showActivityIndicator:(BOOL)show 156 | indicatorStyle:(UIActivityIndicatorViewStyle)style; 157 | 158 | #pragma mark state 159 | 160 | /** 161 | * This method tests, if a notification is currently displayed. 162 | * 163 | * @return YES, if a notification is currently displayed. Otherwise NO. 164 | */ 165 | + (BOOL)isVisible; 166 | 167 | @end 168 | 169 | 170 | -------------------------------------------------------------------------------- /v2ex/Vendor/JDStatusBarNotification/JDStatusBarStyle.h: -------------------------------------------------------------------------------- 1 | // 2 | // JDStatusBarStyle.h 3 | // JDStatusBarNotificationExample 4 | // 5 | // Created by Markus on 04.12.13. 6 | // Copyright (c) 2013 Markus. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | extern NSString *const JDStatusBarStyleError; /// This style has a red background with a white Helvetica label. 13 | extern NSString *const JDStatusBarStyleWarning; /// This style has a yellow background with a gray Helvetica label. 14 | extern NSString *const JDStatusBarStyleSuccess; /// This style has a green background with a white Helvetica label. 15 | extern NSString *const JDStatusBarStyleMatrix; /// This style has a black background with a green bold Courier label. 16 | extern NSString *const JDStatusBarStyleDefault; /// This style has a white background with a gray Helvetica label. 17 | extern NSString *const JDStatusBarStyleDark; /// This style has a nearly black background with a nearly white Helvetica label. 18 | 19 | typedef NS_ENUM(NSInteger, JDStatusBarAnimationType) { 20 | JDStatusBarAnimationTypeNone, /// Notification won't animate 21 | JDStatusBarAnimationTypeMove, /// Notification will move in from the top, and move out again to the top 22 | JDStatusBarAnimationTypeBounce, /// Notification will fall down from the top and bounce a little bit 23 | JDStatusBarAnimationTypeFade /// Notification will fade in and fade out 24 | }; 25 | 26 | typedef NS_ENUM(NSInteger, JDStatusBarProgressBarPosition) { 27 | JDStatusBarProgressBarPositionBottom, /// progress bar will be at the bottom of the status bar 28 | JDStatusBarProgressBarPositionCenter, /// progress bar will be at the center of the status bar 29 | JDStatusBarProgressBarPositionTop, /// progress bar will be at the top of the status bar 30 | JDStatusBarProgressBarPositionBelow, /// progress bar will be below the status bar (the prograss bar won't move with the statusbar in this case) 31 | JDStatusBarProgressBarPositionNavBar, /// progress bar will be below the navigation bar (the prograss bar won't move with the statusbar in this case) 32 | }; 33 | 34 | /** 35 | * A Style defines the appeareance of a notification. 36 | */ 37 | @interface JDStatusBarStyle : NSObject 38 | 39 | /// The background color of the notification bar 40 | @property (nonatomic, strong) UIColor *barColor; 41 | 42 | /// The text color of the notification label 43 | @property (nonatomic, strong) UIColor *textColor; 44 | 45 | /// The text shadow of the notification label 46 | @property (nonatomic, strong) NSShadow *textShadow; 47 | 48 | /// The font of the notification label 49 | @property (nonatomic, strong) UIFont *font; 50 | 51 | /// A correction of the vertical label position in points. Default is 0.0 52 | @property (nonatomic, assign) CGFloat textVerticalPositionAdjustment; 53 | 54 | #pragma mark Animation 55 | 56 | /// The animation, that is used to present the notification 57 | @property (nonatomic, assign) JDStatusBarAnimationType animationType; 58 | 59 | #pragma mark Progress Bar 60 | 61 | /// The background color of the progress bar (on top of the notification bar) 62 | @property (nonatomic, strong) UIColor *progressBarColor; 63 | 64 | /// The height of the progress bar. Default is 1.0 65 | @property (nonatomic, assign) CGFloat progressBarHeight; 66 | 67 | /// The position of the progress bar. Default is JDStatusBarProgressBarPositionBottom 68 | @property (nonatomic, assign) JDStatusBarProgressBarPosition progressBarPosition; 69 | 70 | @end 71 | 72 | -------------------------------------------------------------------------------- /v2ex/Vendor/JDStatusBarNotification/JDStatusBarStyle.m: -------------------------------------------------------------------------------- 1 | // 2 | // JDStatusBarStyle.m 3 | // JDStatusBarNotificationExample 4 | // 5 | // Created by Markus on 04.12.13. 6 | // Copyright (c) 2013 Markus. All rights reserved. 7 | // 8 | 9 | #import "JDStatusBarStyle.h" 10 | 11 | NSString *const JDStatusBarStyleError = @"JDStatusBarStyleError"; 12 | NSString *const JDStatusBarStyleWarning = @"JDStatusBarStyleWarning"; 13 | NSString *const JDStatusBarStyleSuccess = @"JDStatusBarStyleSuccess"; 14 | NSString *const JDStatusBarStyleMatrix = @"JDStatusBarStyleMatrix"; 15 | NSString *const JDStatusBarStyleDefault = @"JDStatusBarStyleDefault"; 16 | NSString *const JDStatusBarStyleDark = @"JDStatusBarStyleDark"; 17 | 18 | @implementation JDStatusBarStyle 19 | 20 | - (instancetype)copyWithZone:(NSZone*)zone; 21 | { 22 | JDStatusBarStyle *style = [[[self class] allocWithZone:zone] init]; 23 | style.barColor = self.barColor; 24 | style.textColor = self.textColor; 25 | style.textShadow = self.textShadow; 26 | style.font = self.font; 27 | style.textVerticalPositionAdjustment = self.textVerticalPositionAdjustment; 28 | style.animationType = self.animationType; 29 | style.progressBarColor = self.progressBarColor; 30 | style.progressBarHeight = self.progressBarHeight; 31 | style.progressBarPosition = self.progressBarPosition; 32 | return style; 33 | } 34 | 35 | + (NSArray*)allDefaultStyleIdentifier; 36 | { 37 | return @[JDStatusBarStyleError, JDStatusBarStyleWarning, 38 | JDStatusBarStyleSuccess, JDStatusBarStyleMatrix, 39 | JDStatusBarStyleDark]; 40 | } 41 | 42 | + (JDStatusBarStyle*)defaultStyleWithName:(NSString*)styleName; 43 | { 44 | // setup default style 45 | JDStatusBarStyle *style = [[JDStatusBarStyle alloc] init]; 46 | style.barColor = [UIColor whiteColor]; 47 | style.progressBarColor = [UIColor greenColor]; 48 | style.progressBarHeight = 1.0; 49 | style.progressBarPosition = JDStatusBarProgressBarPositionBottom; 50 | style.textColor = [UIColor grayColor]; 51 | style.font = [UIFont systemFontOfSize:12.0]; 52 | style.animationType = JDStatusBarAnimationTypeMove; 53 | 54 | // JDStatusBarStyleDefault 55 | if ([styleName isEqualToString:JDStatusBarStyleDefault]) { 56 | return style; 57 | } 58 | 59 | // JDStatusBarStyleError 60 | else if ([styleName isEqualToString:JDStatusBarStyleError]) { 61 | style.barColor = [UIColor colorWithRed:0.588 green:0.118 blue:0.000 alpha:1.000]; 62 | style.textColor = [UIColor whiteColor]; 63 | style.progressBarColor = [UIColor redColor]; 64 | style.progressBarHeight = 2.0; 65 | return style; 66 | } 67 | 68 | // JDStatusBarStyleWarning 69 | else if ([styleName isEqualToString:JDStatusBarStyleWarning]) { 70 | style.barColor = [UIColor colorWithRed:0.900 green:0.734 blue:0.034 alpha:1.000]; 71 | style.textColor = [UIColor darkGrayColor]; 72 | style.progressBarColor = style.textColor; 73 | return style; 74 | } 75 | 76 | // JDStatusBarStyleSuccess 77 | else if ([styleName isEqualToString:JDStatusBarStyleSuccess]) { 78 | style.barColor = [UIColor colorWithRed:0.588 green:0.797 blue:0.000 alpha:1.000]; 79 | style.textColor = [UIColor whiteColor]; 80 | style.progressBarColor = [UIColor colorWithRed:0.106 green:0.594 blue:0.319 alpha:1.000]; 81 | style.progressBarHeight = 1.0+1.0/[[UIScreen mainScreen] scale]; 82 | return style; 83 | } 84 | 85 | // JDStatusBarStyleDark 86 | else if ([styleName isEqualToString:JDStatusBarStyleDark]) { 87 | style.barColor = [UIColor colorWithRed:0.050 green:0.078 blue:0.120 alpha:1.000]; 88 | style.textColor = [UIColor colorWithWhite:0.95 alpha:1.0]; 89 | style.progressBarHeight = 1.0+1.0/[[UIScreen mainScreen] scale]; 90 | return style; 91 | } 92 | 93 | // JDStatusBarStyleMatrix 94 | else if ([styleName isEqualToString:JDStatusBarStyleMatrix]) { 95 | style.barColor = [UIColor blackColor]; 96 | style.textColor = [UIColor greenColor]; 97 | style.font = [UIFont fontWithName:@"Courier-Bold" size:14.0]; 98 | style.progressBarColor = [UIColor greenColor]; 99 | style.progressBarHeight = 2.0; 100 | return style; 101 | } 102 | 103 | return nil; 104 | } 105 | 106 | @end 107 | -------------------------------------------------------------------------------- /v2ex/Vendor/JDStatusBarNotification/JDStatusBarView.h: -------------------------------------------------------------------------------- 1 | // 2 | // JDStatusBarView.h 3 | // JDStatusBarNotificationExample 4 | // 5 | // Created by Markus on 04.12.13. 6 | // Copyright (c) 2013 Markus. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface JDStatusBarView : UIView 12 | @property (nonatomic, strong, readonly) UILabel *textLabel; 13 | @property (nonatomic, strong, readonly) UIActivityIndicatorView *activityIndicatorView; 14 | @property (nonatomic, assign) CGFloat textVerticalPositionAdjustment; 15 | @end 16 | -------------------------------------------------------------------------------- /v2ex/Vendor/JDStatusBarNotification/JDStatusBarView.m: -------------------------------------------------------------------------------- 1 | // 2 | // JDStatusBarView.m 3 | // JDStatusBarNotificationExample 4 | // 5 | // Created by Markus on 04.12.13. 6 | // Copyright (c) 2013 Markus. All rights reserved. 7 | // 8 | 9 | #import "JDStatusBarView.h" 10 | 11 | @interface JDStatusBarView () 12 | @property (nonatomic, strong) UILabel *textLabel; 13 | @property (nonatomic, strong) UIActivityIndicatorView *activityIndicatorView; 14 | @end 15 | 16 | @implementation JDStatusBarView 17 | 18 | #pragma mark dynamic getter 19 | 20 | - (UILabel *)textLabel; 21 | { 22 | if (_textLabel == nil) { 23 | _textLabel = [[UILabel alloc] init]; 24 | _textLabel.backgroundColor = [UIColor clearColor]; 25 | _textLabel.baselineAdjustment = UIBaselineAdjustmentAlignCenters; 26 | _textLabel.textAlignment = NSTextAlignmentCenter; 27 | _textLabel.adjustsFontSizeToFitWidth = YES; 28 | _textLabel.clipsToBounds = YES; 29 | [self addSubview:_textLabel]; 30 | } 31 | return _textLabel; 32 | } 33 | 34 | - (UIActivityIndicatorView *)activityIndicatorView; 35 | { 36 | if (_activityIndicatorView == nil) { 37 | _activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite]; 38 | _activityIndicatorView.transform = CGAffineTransformMakeScale(0.7, 0.7); 39 | [self addSubview:_activityIndicatorView]; 40 | } 41 | return _activityIndicatorView; 42 | } 43 | 44 | #pragma mark setter 45 | 46 | - (void)setTextVerticalPositionAdjustment:(CGFloat)textVerticalPositionAdjustment; 47 | { 48 | _textVerticalPositionAdjustment = textVerticalPositionAdjustment; 49 | [self setNeedsLayout]; 50 | } 51 | 52 | #pragma mark layout 53 | 54 | - (void)layoutSubviews; 55 | { 56 | [super layoutSubviews]; 57 | 58 | // label 59 | self.textLabel.frame = CGRectMake(0, 1+self.textVerticalPositionAdjustment, 60 | self.bounds.size.width, self.bounds.size.height-1); 61 | 62 | // activity indicator 63 | if (_activityIndicatorView ) { 64 | CGSize textSize = [self currentTextSize]; 65 | CGRect indicatorFrame = _activityIndicatorView.frame; 66 | indicatorFrame.origin.x = round((self.bounds.size.width - textSize.width)/2.0) - indicatorFrame.size.width - 8.0; 67 | indicatorFrame.origin.y = ceil(1+(self.bounds.size.height - indicatorFrame.size.height)/2.0); 68 | _activityIndicatorView.frame = indicatorFrame; 69 | } 70 | } 71 | 72 | - (CGSize)currentTextSize; 73 | { 74 | CGSize textSize = CGSizeZero; 75 | 76 | // use new sizeWithAttributes: if possible 77 | SEL selector = NSSelectorFromString(@"sizeWithAttributes:"); 78 | if ([self.textLabel.text respondsToSelector:selector]) { 79 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000 80 | NSDictionary *attributes = @{NSFontAttributeName:self.textLabel.font}; 81 | textSize = [self.textLabel.text sizeWithAttributes:attributes]; 82 | #endif 83 | } 84 | 85 | // otherwise use old sizeWithFont: 86 | else { 87 | #if __IPHONE_OS_VERSION_MIN_REQUIRED < 70000 // only when deployment target is < ios7 88 | textSize = [self.textLabel.text sizeWithFont:self.textLabel.font]; 89 | #endif 90 | } 91 | 92 | return textSize; 93 | } 94 | 95 | @end 96 | -------------------------------------------------------------------------------- /v2ex/Vendor/TDBadgedCell/TDBadgedCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // TDBadgedCell.h 3 | // TDBadgedTableCell 4 | // TDBageView 5 | // 6 | // Any rereleasing of this code is prohibited. 7 | // Please attribute use of this code within your application 8 | // 9 | // Any Queries should be directed to hi@tmdvs.me | http://www.tmdvs.me 10 | // 11 | // Created by Tim 12 | // Copyright 2011 Tim Davies. All rights reserved. 13 | // 14 | 15 | #import 16 | #import 17 | 18 | #ifndef TD_STRONG 19 | #if __has_feature(objc_arc) 20 | #define TD_STRONG strong 21 | #else 22 | #define TD_STRONG retain 23 | #endif 24 | #endif 25 | 26 | #ifndef TD_WEAK 27 | #if __has_feature(objc_arc_weak) 28 | #define TD_WEAK weak 29 | #elif __has_feature(objc_arc) 30 | #define TD_WEAK unsafe_unretained 31 | #else 32 | #define TD_WEAK assign 33 | #endif 34 | #endif 35 | 36 | @interface TDBadgeView : UIView 37 | { 38 | UIColor *__defaultColor; 39 | UIColor *__defaultHighlightColor; 40 | } 41 | 42 | @property (nonatomic, readonly) NSUInteger width; 43 | @property (nonatomic, TD_STRONG) NSString *badgeString; 44 | @property (nonatomic, TD_WEAK) UITableViewCell *parent; 45 | @property (nonatomic, TD_STRONG) UIColor *badgeColor; 46 | @property (nonatomic, TD_STRONG) UIColor *badgeTextColor; 47 | @property (nonatomic, TD_STRONG) UIColor *badgeColorHighlighted; 48 | @property (nonatomic, TD_STRONG) UIColor *badgeTextColorHighlighted; 49 | @property (nonatomic, assign) BOOL boldFont; 50 | @property (nonatomic, assign) CGFloat fontSize; 51 | @property (nonatomic, assign) CGFloat radius; 52 | 53 | @end 54 | 55 | @interface TDBadgedCell : UITableViewCell { 56 | 57 | } 58 | 59 | @property (nonatomic, TD_STRONG) NSString *badgeString; 60 | @property (readonly, TD_STRONG) TDBadgeView *badge; 61 | @property (nonatomic, TD_STRONG) UIColor *badgeColor; 62 | @property (nonatomic, TD_STRONG) UIColor *badgeTextColor; 63 | @property (nonatomic, TD_STRONG) UIColor *badgeColorHighlighted; 64 | @property (nonatomic, TD_STRONG) UIColor *badgeTextColorHighlighted; 65 | @property (nonatomic, assign) CGFloat badgeLeftOffset; 66 | @property (nonatomic, assign) CGFloat badgeRightOffset; 67 | @property (nonatomic, assign) CGFloat badgeHorizPadding; 68 | @property (nonatomic, assign) CGFloat badgeVertPadding; 69 | @property (nonatomic, TD_STRONG) NSMutableArray *resizeableLabels; 70 | 71 | @end 72 | -------------------------------------------------------------------------------- /v2ex/v2ex-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // v2ex-Bridging-Header.h 3 | // v2ex 4 | // 5 | // Created by zhenwen on 6/25/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | #ifndef v2ex_v2ex_Bridging_Header_h 10 | #define v2ex_v2ex_Bridging_Header_h 11 | 12 | #import 13 | #import "Flurry.h" 14 | #import "JDStatusBarNotification.h" 15 | #import "TDBadgedCell.h" 16 | #import "DAKeyboardControl.h" 17 | 18 | #endif 19 | -------------------------------------------------------------------------------- /v2ex/v2ex.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.ccc.shitoudev.v2ex 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /v2ex/v2ex.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | //: Playground - noun: a place where people can play 2 | 3 | import Cocoa 4 | 5 | var str = "Hello, playground" 6 | 7 | extension Double { 8 | var km: Double { return self * 1_000.0 } 9 | var m: Double { return self } 10 | func cm() -> Double { 11 | return self / 100.0 12 | } 13 | } 14 | 15 | let oneInch = 25.4.km 16 | oneInch.m 17 | oneInch.cm() 18 | 19 | extension Int { 20 | func repetitions(task: () -> ()) { 21 | for i in 0.. Double 79 | } 80 | 81 | class LinearCongruentialGenerator: RandomNumberGenerator { 82 | var lastRandom = 42.0 83 | let m = 139968.0 84 | let a = 3877.0 85 | let c = 29573.0 86 | func random() -> Double { 87 | lastRandom = ((lastRandom * a + c) % m) 88 | return lastRandom / m 89 | } 90 | } 91 | 92 | class Dice { 93 | let sides: Int 94 | let generator: RandomNumberGenerator 95 | init(sides: Int, generator: RandomNumberGenerator) { 96 | self.sides = sides 97 | self.generator = generator 98 | } 99 | func roll() -> Int { 100 | return Int(generator.random() * Double(sides)) + 1 101 | } 102 | } 103 | 104 | var d6 = Dice(sides: 6,generator: LinearCongruentialGenerator()) 105 | for _ in 1...5 { 106 | println("Random dice roll is \(d6.roll())") 107 | } 108 | 109 | protocol TextRepresentable { 110 | func asText() -> String 111 | } 112 | 113 | extension Dice : TextRepresentable { 114 | func asText() -> String { 115 | return "A \(sides)-sided dice" 116 | } 117 | } 118 | let d12 = Dice(sides: 12,generator: LinearCongruentialGenerator()) 119 | println(d12.asText()) 120 | 121 | struct Hamster { 122 | var name: String 123 | func asText() -> String { 124 | return "A hamster named \(name)" 125 | } 126 | } 127 | extension Hamster: TextRepresentable {} 128 | var hamster: TextRepresentable = Hamster(name: "Hamster!") 129 | hamster.asText() 130 | 131 | let things: [TextRepresentable] = [d12,hamster] 132 | for thing in things { 133 | thing.asText() 134 | if let dice = thing as? Dice { 135 | dice.sides 136 | } 137 | } 138 | 139 | // 协议合成 140 | protocol Named { 141 | var name: String { get } 142 | } 143 | protocol Aged { 144 | var age: Int { get } 145 | } 146 | struct Person: Named, Aged { 147 | var name: String 148 | var age: Int 149 | } 150 | func wishHappyBirthday(celebrator: protocol) { 151 | println("Happy birthday \(celebrator.name) - you're \(celebrator.age)!") 152 | } 153 | let birthdayPerson = Person(name: "mks", age: 21) 154 | wishHappyBirthday(birthdayPerson) 155 | 156 | // 可选协议 157 | @objc protocol CounterDataSource { 158 | optional func incrementForCount(count: Int) -> Int 159 | optional var fixedIncrement: Int { get } 160 | } 161 | // 泛型 162 | func swapTwoInts(inout a: Int, inout b: Int) { 163 | let temporaryA = a 164 | a = b 165 | b = temporaryA 166 | } 167 | var someInt = 3 168 | var anotherInt = 107 169 | swapTwoInts(&someInt, &anotherInt) 170 | println("someInt is now \(someInt), and anotherInt is now \(anotherInt)") 171 | 172 | func swapTwoValues(inout a: T, inout b: T) { 173 | let temporaryA = a 174 | a = b 175 | b = temporaryA 176 | } 177 | swapTwoValues(&someInt, &anotherInt) 178 | 179 | struct IntStack { 180 | var items = [Int]() 181 | mutating func push(item: Int) { 182 | items.append(item) 183 | } 184 | mutating func pop() -> Int { 185 | return items.removeLast() 186 | } 187 | } 188 | struct Stack { 189 | var items = [T]() 190 | mutating func push(item: T) { 191 | items.append(item) 192 | } 193 | mutating func pop() -> T { 194 | return items.removeLast() 195 | } 196 | } 197 | 198 | var stackItems = Stack(items: [1,2,3]) 199 | stackItems.pop() 200 | 201 | var arr = [Int:String]() 202 | for (index, value) in enumerate(arr) { 203 | 204 | } 205 | 206 | func foundIndex (array: [T], valueToFind: T) -> Int? { 207 | return nil 208 | } 209 | 210 | protocol Container { 211 | typealias ItemType 212 | mutating func append(item: ItemType) 213 | var count: Int { get } 214 | subscript(i: Int) -> ItemType { get } 215 | } 216 | struct IntsStack:Container { 217 | var items = [Int]() 218 | mutating func push(item: Int) { 219 | items.append(item) 220 | } 221 | mutating func pop() -> Int { 222 | return items.removeLast() 223 | } 224 | 225 | // Container 226 | typealias ItemType = Int 227 | mutating func append(item: Int) { 228 | items.append(item) 229 | } 230 | var count: Int { 231 | return items.count 232 | } 233 | subscript(i: Int) -> Int { 234 | return items[i] 235 | } 236 | } 237 | 238 | extension Array: Container{} 239 | // 元祖 240 | let http200Status = (statusCode: 200, desc: "OK") 241 | 242 | let one: UInt16 = 0b00001111 243 | let onef = ~one 244 | 245 | class A { 246 | let b: B 247 | init() { 248 | b = B() 249 | b.a = self 250 | } 251 | 252 | deinit { 253 | println("A deinit") 254 | } 255 | } 256 | 257 | class B { 258 | weak var a: A? = nil 259 | deinit { 260 | println("B deinit") 261 | } 262 | } 263 | 264 | var obj: A? = A() 265 | obj = nil 266 | 267 | 268 | -------------------------------------------------------------------------------- /v2ex/v2ex.playground/Sources/SupportCode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file (and all other Swift source files in the Sources directory of this playground) will be precompiled into a framework which is automatically made available to v2ex.playground. 3 | // 4 | -------------------------------------------------------------------------------- /v2ex/v2ex.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /v2ex/v2ex.playground/timeline.xctimeline: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /v2exKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /v2exKit/STColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // STColor.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 5/10/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIColor { 12 | 13 | static public func colorWithHexString (hex:String) -> UIColor { 14 | var cString:String = hex.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet() as NSCharacterSet).uppercaseString 15 | 16 | if (cString.hasPrefix("#")) { 17 | // cString = cString.substringFromIndex(advance(cString.startIndex, 1)) 18 | cString = (cString as NSString).substringFromIndex(1) 19 | } 20 | 21 | if (cString.characters.count != 6) { 22 | return UIColor.grayColor() 23 | } 24 | 25 | var rgbValue:UInt32 = 0 26 | NSScanner(string: cString).scanHexInt(&rgbValue) 27 | 28 | return UIColor( 29 | red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0, 30 | green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0, 31 | blue: CGFloat(rgbValue & 0x0000FF) / 255.0, 32 | alpha: CGFloat(1.0) 33 | ) 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /v2exKit/STDate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // STDate.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 5/11/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension String { 12 | 13 | public static func smartDate (strtotime: NSTimeInterval) -> String { 14 | var smartStr = "" 15 | 16 | let currentDate = NSDate().timeIntervalSince1970 17 | let endDate = NSDate(timeIntervalSince1970: strtotime) 18 | 19 | let distanceTime = currentDate - strtotime 20 | let dateFormatter = NSDateFormatter() 21 | 22 | // println("currentDate = \(currentDate), strtotime \(strtotime), distanceTime = \(distanceTime)") 23 | 24 | if distanceTime < 60 { 25 | //20分钟以内 26 | smartStr = "刚刚" 27 | }else if distanceTime >= 1*60 && distanceTime < 60*60 { 28 | //20分钟~1小时 29 | smartStr = NSString(format: "%d分钟之前", Int(distanceTime/60)) as String 30 | }else if distanceTime >= 60*60 && distanceTime < 24*60*60 { 31 | //1~24小时 32 | smartStr = NSString(format: "%d小时之前", Int(distanceTime/(60*60))) as String 33 | }else if distanceTime >= 24*60*60 && distanceTime < 2*24*60*60 { 34 | //昨天 35 | dateFormatter.dateFormat = "HH:mm" 36 | smartStr = NSString(format: "昨天 %s", dateFormatter.stringFromDate(endDate)) as String 37 | }else if distanceTime >= 2*24*60*60 && distanceTime < 3*24*60*60 { 38 | //前天 39 | dateFormatter.dateFormat = "HH:mm" 40 | smartStr = NSString(format: "前天 %s", dateFormatter.stringFromDate(endDate)) as String 41 | }else if distanceTime >= 3*24*60*60 && distanceTime < 365*24*60*60 { 42 | //一年之内 43 | dateFormatter.dateFormat = "MM/dd HH:mm" 44 | smartStr = dateFormatter.stringFromDate(endDate) as String 45 | }else if distanceTime >= 365*24*60*60 { 46 | //一年之外 47 | dateFormatter.dateFormat = "yyyy/MM/dd HH:mm" 48 | smartStr = dateFormatter.stringFromDate(endDate) as String 49 | } 50 | 51 | return smartStr 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /v2exKit/STInsetLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // STInsetLabel.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 7/31/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class STInsetLabel: UILabel { 12 | 13 | var padding = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10) //UIEdgeInsetsZero 14 | 15 | override init(frame: CGRect) { 16 | super.init(frame: frame) 17 | } 18 | 19 | required init(coder aDecoder: NSCoder) { 20 | super.init(coder: aDecoder)! 21 | } 22 | 23 | internal convenience init(frame: CGRect, inset:UIEdgeInsets) { 24 | self.init(frame: frame) 25 | self.padding = inset 26 | } 27 | 28 | override func drawTextInRect(rect: CGRect) { 29 | super.drawTextInRect(UIEdgeInsetsInsetRect(rect, padding)) 30 | } 31 | 32 | override func intrinsicContentSize() -> CGSize { 33 | var size = super.intrinsicContentSize() 34 | size.width += padding.left + padding.right 35 | size.height += padding.top + padding.bottom 36 | return size 37 | } 38 | } -------------------------------------------------------------------------------- /v2exKit/STString.swift: -------------------------------------------------------------------------------- 1 | // 2 | // STString.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 6/8/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension String { 12 | 13 | public static func strHeight (str: String, size: CGSize, font: UIFont) -> CGFloat { 14 | // NSStringDrawingOptions.UsesFontLeading 15 | let rect: CGRect = str.boundingRectWithSize(size, options: [.UsesLineFragmentOrigin, .UsesFontLeading], attributes: [NSFontAttributeName:font], context: nil) 16 | return rect.size.height 17 | } 18 | 19 | public func trim() -> String { 20 | return self.stringByTrimmingCharactersInSet(.whitespaceCharacterSet()) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /v2exKit/STTextView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // STTextView.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 6/17/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class STTextView: UITextView { 12 | 13 | public var placeHolder: NSString! { 14 | didSet { 15 | self.setNeedsDisplay() 16 | } 17 | } 18 | public var placeHolderTextColor: UIColor! { 19 | didSet { 20 | self.setNeedsDisplay() 21 | } 22 | } 23 | 24 | // MARK: - init 25 | override init(frame: CGRect, textContainer: NSTextContainer?) { 26 | super.init(frame: frame, textContainer: textContainer) 27 | self.configureTextView() 28 | } 29 | 30 | required public init(coder aDecoder: NSCoder) { 31 | super.init(coder: aDecoder)! 32 | self.configureTextView() 33 | } 34 | 35 | override public func awakeFromNib() { 36 | super.awakeFromNib() 37 | self.configureTextView() 38 | } 39 | 40 | override public func layoutSubviews() { 41 | super.layoutSubviews() 42 | setNeedsDisplay() 43 | } 44 | 45 | override public func drawRect(rect: CGRect) { 46 | super.drawRect(rect) 47 | 48 | if text.characters.count == 0 && self.placeHolder != nil { 49 | self.placeHolderTextColor.set() 50 | 51 | self.placeHolder.drawInRect(CGRectInset(rect, 7.0, 6.0), withAttributes: self.placeholderTextAttributes()) 52 | } 53 | } 54 | 55 | func configureTextView() -> Void { 56 | 57 | let cornerRadius: CGFloat = 5.0 58 | 59 | self.scrollEnabled = true 60 | self.scrollsToTop = false 61 | 62 | self.layer.borderWidth = 0.5 63 | self.layer.borderColor = UIColor.lightGrayColor().CGColor 64 | self.layer.cornerRadius = cornerRadius 65 | self.scrollIndicatorInsets = UIEdgeInsets(top: cornerRadius, left: 0, bottom: cornerRadius, right: 0) 66 | 67 | self.textContainerInset = UIEdgeInsets(top: 3, left: 2, bottom: 3, right: 2) 68 | self.contentInset = UIEdgeInsets(top: 3, left: 0, bottom: 3, right: 0) 69 | 70 | self.backgroundColor = UIColor.whiteColor() 71 | self.font = UIFont.systemFontOfSize(13) 72 | self.placeHolder = "添加评论..." 73 | self.placeHolderTextColor = UIColor.lightGrayColor() 74 | 75 | self.addTextViewNotificationObservers() 76 | 77 | } 78 | 79 | func placeholderTextAttributes() -> [String : AnyObject] { 80 | let paragraphStyle = NSMutableParagraphStyle() 81 | paragraphStyle.lineBreakMode = NSLineBreakMode.ByTruncatingTail 82 | paragraphStyle.alignment = self.textAlignment; 83 | 84 | let attr = [NSFontAttributeName: font!, NSForegroundColorAttributeName: placeHolderTextColor, NSParagraphStyleAttributeName: paragraphStyle] 85 | 86 | return attr 87 | } 88 | 89 | func addTextViewNotificationObservers() -> Void { 90 | NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(didReceiveTextViewNotification(_:)), name: UITextViewTextDidChangeNotification, object: self) 91 | NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(didReceiveTextViewNotification(_:)), name: UITextViewTextDidBeginEditingNotification, object: self) 92 | NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(didReceiveTextViewNotification(_:)), name: UITextViewTextDidEndEditingNotification, object: self) 93 | } 94 | 95 | func removeTextViewNotificationObservers() -> Void { 96 | NSNotificationCenter.defaultCenter().removeObserver(self, name: UITextViewTextDidChangeNotification, object: self) 97 | NSNotificationCenter.defaultCenter().removeObserver(self, name: UITextViewTextDidBeginEditingNotification, object: self) 98 | NSNotificationCenter.defaultCenter().removeObserver(self, name: UITextViewTextDidEndEditingNotification, object: self) 99 | } 100 | 101 | func didReceiveTextViewNotification(notification: NSNotification) { 102 | setNeedsDisplay() 103 | } 104 | 105 | // MARK: Lifetime 106 | 107 | deinit { 108 | removeTextViewNotificationObservers() 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /v2exKit/STView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // STView.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 6/7/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIView { 12 | 13 | public var left: CGFloat { 14 | get { 15 | return self.frame.origin.x 16 | } 17 | set { 18 | var frame = self.frame 19 | frame.origin.x = newValue 20 | self.frame = frame 21 | } 22 | } 23 | public var right: CGFloat { 24 | return self.left + self.width 25 | } 26 | 27 | public var top: CGFloat { 28 | return self.frame.origin.y 29 | } 30 | public var bottom: CGFloat { 31 | return self.top + self.height 32 | } 33 | 34 | public var width: CGFloat { 35 | return self.frame.size.width 36 | } 37 | public var height: CGFloat { 38 | return self.frame.size.height 39 | } 40 | 41 | public func traverseResponderChainForUIViewController() -> UIViewController? { 42 | 43 | if let nextResponder = self.nextResponder() { 44 | if nextResponder.isKindOfClass(UIViewController) { 45 | return (nextResponder as! UIViewController) 46 | }else if (nextResponder.isKindOfClass(UIView)){ 47 | let view = nextResponder as! UIView 48 | return view.traverseResponderChainForUIViewController() 49 | }else{ 50 | return nil 51 | } 52 | }else{ 53 | return nil 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /v2exKit/v2exKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // v2exKit.h 3 | // v2exKit 4 | // 5 | // Created by zhenwen on 6/24/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for v2exKit. 12 | FOUNDATION_EXPORT double v2exKitVersionNumber; 13 | 14 | //! Project version string for v2exKit. 15 | FOUNDATION_EXPORT const unsigned char v2exKitVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import -------------------------------------------------------------------------------- /v2exKit/v2exKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // v2exKit.swift 3 | // v2ex 4 | // 5 | // Created by zhenwen on 6/25/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public let kAppGroupIdentifier = "group.ccc.shitoudev.v2ex" 12 | public let kAppSharedDefaultsTodayExtensionDataKey = "cc.yueti.today.extension" 13 | public let kAppPostScheme = "v2ex://post/?postId=%d" 14 | 15 | public let kLinkColor = "#778087" 16 | public let kAppNormalColor = UIColor.colorWithHexString("#333344") 17 | public let kIsiPad = UIDevice.currentDevice().userInterfaceIdiom == UIUserInterfaceIdiom.Pad 18 | public let kContentFont = kIsiPad ? UIFont.systemFontOfSize(16) : UIFont.systemFontOfSize(14) 19 | public let kTitleFont = kIsiPad ? UIFont.systemFontOfSize(16) : UIFont.systemFontOfSize(14) 20 | 21 | public let htmlRegularExpression = try! NSRegularExpression(pattern: "<[^>]+>", options: [.CaseInsensitive]) 22 | 23 | public var defaultTableFooterView: UIView { 24 | let footerView = UIView() 25 | footerView.backgroundColor = UIColor.clearColor() 26 | return footerView 27 | } 28 | 29 | 30 | /** 31 | 修改链接的文字属性 32 | 33 | :param: attrStr 内容源 34 | :param: range 修改的内容范围 35 | 36 | :returns: 修改之后的内容 37 | */ 38 | public func addLinkAttributed(attrStr: NSMutableAttributedString, range: NSRange) -> NSMutableAttributedString { 39 | 40 | attrStr.removeAttribute(kCTForegroundColorAttributeName as String, range: range) 41 | attrStr.addAttribute(kCTForegroundColorAttributeName as String, value: UIColor.colorWithHexString(kLinkColor).CGColor, range: range) 42 | 43 | return attrStr 44 | } -------------------------------------------------------------------------------- /v2exKitTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /v2exKitTests/v2exKitTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // v2exKitTests.swift 3 | // v2exKitTests 4 | // 5 | // Created by zhenwen on 6/24/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | 12 | class v2exKitTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | XCTAssert(true, "Pass") 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measureBlock() { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /v2exTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /v2exTests/v2exTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // v2exTests.swift 3 | // v2exTests 4 | // 5 | // Created by zhenwen on 5/2/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | 12 | class v2exTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | XCTAssert(true, "Pass") 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measureBlock() { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /v2exTodayExtension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | v2ex 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | XPC! 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | NSExtension 26 | 27 | NSExtensionMainStoryboard 28 | MainInterface 29 | NSExtensionPointIdentifier 30 | com.apple.widget-extension 31 | 32 | NSAppTransportSecurity 33 | 34 | NSExceptionDomains 35 | 36 | v2ex.co 37 | 38 | NSExceptionAllowsInsecureHTTPLoads 39 | 40 | NSExceptionMinimumTLSVersion 41 | TLSv1.1 42 | NSIncludesSubdomains 43 | 44 | 45 | v2ex.com 46 | 47 | NSExceptionAllowsInsecureHTTPLoads 48 | 49 | NSExceptionMinimumTLSVersion 50 | TLSv1.1 51 | NSIncludesSubdomains 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /v2exTodayExtension/MainInterface.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /v2exTodayExtension/TodayViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodayViewController.swift 3 | // v2exTodayExtension 4 | // 5 | // Created by zhenwen on 6/22/15. 6 | // Copyright (c) 2015 zhenwen. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import NotificationCenter 11 | import v2exKit 12 | 13 | class TodayViewController: UIViewController, NCWidgetProviding { 14 | 15 | @IBOutlet weak var tableView: UITableView! 16 | var dataSouce = [] { 17 | didSet { 18 | tableView.reloadData() 19 | let height = Int(tableView.rowHeight) * dataSouce.count 20 | preferredContentSize = CGSize(width: view.width, height: CGFloat(height)) 21 | } 22 | } 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | // Do any additional setup after loading the view from its nib. 27 | 28 | tableView.delegate = self 29 | tableView.dataSource = self 30 | tableView.rowHeight = 44 31 | 32 | reloadTableViewData(nil) 33 | refresh(nil) 34 | } 35 | 36 | override func didReceiveMemoryWarning() { 37 | super.didReceiveMemoryWarning() 38 | // Dispose of any resources that can be recreated. 39 | } 40 | 41 | func widgetMarginInsetsForProposedMarginInsets(defaultMarginInsets: UIEdgeInsets) -> UIEdgeInsets { 42 | return UIEdgeInsets(top: 0, left: 8, bottom: 15, right: 0) 43 | } 44 | 45 | func widgetPerformUpdateWithCompletionHandler(completionHandler: ((NCUpdateResult) -> Void)) { 46 | // Perform any setup necessary in order to update the view. 47 | 48 | // If an error is encountered, use NCUpdateResult.Failed 49 | // If there's no update required, use NCUpdateResult.NoData 50 | // If there's an update, use NCUpdateResult.NewData 51 | 52 | // 从网络获取数据 53 | refresh(completionHandler) 54 | } 55 | 56 | func refresh(completionHandler: ((NCUpdateResult) -> Void)?) { 57 | PostModel.getPostList(PostType.Navi, target: "hot") { (obj, error) -> Void in 58 | self.reloadTableViewData(completionHandler) 59 | } 60 | } 61 | 62 | func reloadTableViewData(completionHandler: ((NCUpdateResult) -> Void)?) { 63 | var result = NCUpdateResult.NoData 64 | let userDefaults = NSUserDefaults(suiteName: kAppGroupIdentifier) 65 | let data = userDefaults?.objectForKey(kAppSharedDefaultsTodayExtensionDataKey) as? NSArray 66 | if data?.count > 0, let souceFirst = dataSouce.firstObject{ 67 | let first = data!.firstObject 68 | if souceFirst["id"] as! Int != first!["id"] as! Int { 69 | result = .NewData 70 | } 71 | } 72 | self.dataSouce = data?.count > 0 ? data! : [] 73 | completionHandler?(result) 74 | } 75 | 76 | } 77 | 78 | // MARK: UITableViewDataSource & UITableViewDelegate 79 | extension TodayViewController: UITableViewDelegate, UITableViewDataSource { 80 | // UITableViewDataSource 81 | func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 82 | let cell: UITableViewCell = tableView.dequeueReusableCellWithIdentifier("todayPostCellId")! 83 | 84 | let post = dataSouce[indexPath.row] as! NSDictionary 85 | cell.textLabel?.text = post["title"] as? String 86 | cell.textLabel?.textColor = UIColor.whiteColor() 87 | cell.textLabel?.font = UIFont.systemFontOfSize(14) 88 | 89 | return cell 90 | } 91 | 92 | func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 93 | return dataSouce.count; 94 | } 95 | 96 | // UITableViewDelegate 97 | func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { 98 | tableView.deselectRowAtIndexPath(indexPath, animated: true) 99 | let post = dataSouce[indexPath.row] as! NSDictionary 100 | let postId = post["id"]! as! Int 101 | let url = String(format: kAppPostScheme, arguments: [postId]) 102 | self.extensionContext?.openURL(NSURL(string: url)!, completionHandler: { (succ) -> Void in 103 | 104 | }) 105 | } 106 | } 107 | 108 | -------------------------------------------------------------------------------- /v2exTodayExtension/v2exTodayExtension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.ccc.shitoudev.v2ex 8 | 9 | 10 | 11 | --------------------------------------------------------------------------------