移动端用户反馈工具
价值
一个高效的用户反馈工具,用户遇到问题只需手机摇一摇并标注,相关数据即可反馈给开发。
- 应用里所见即所得的提交Bug,体验流畅,方便快捷;
- 自动获取Bug产生时的界面截图;
- 直接在应用截图中进行涂改和描述问题,所提即所指;
- 自动获取移动设备的硬件参数(型号,系统版本,屏幕分辨率等);
- 自动获取应用运行上下文数据(如:网络状态,用户数据,日志信息,用户操作步骤等);
- 结合原有的IM对话反馈渠道,可以直接对用户发起沟通;
背景
收集用户对应用的反馈至关重要,无论它是需要修复的错误,需要添加的功能还是需要解决的用户体验问题。但我们缺少一种合适的方式来倾听用户对应用程序的反馈。
通常,一个 Bug 从被发现到最终提交到开发解决,要经历如下步骤:
- 发现问题
- 手动截屏
- 手机连电脑
- 把图导入电脑
- 记录设备信息
- 尽可能地描述 Bug 及其复现步骤
- 提交到系统,指派解决人
这样复杂的流程导致普通用户在面对一般bug时几乎没有耐心,他们会在重试一到两次之后关闭App。而面对严重bug时又缺乏沟通渠道,只能在应用商店或者其他公开渠道用一种发泄的态度给予差评。
我抓取了APPSotre上的最近一年的评价,149个三星及以下的用户评价中,有31个明确表示是Bug导致,还有17个表示功能需求缺失。
而这31个Bug导致的差评描述非常粗浅:
截屏没有,设备信息不全,问题描述也不够准确,现场数据缺失、Bug 难重现、问题难以定位和解决,就连用户都非常难以联系。
总结现状:移动应用 Bug 难反馈、难描述、难复现、难解决。
目的
建立一个高效的反馈系统,在私有渠道收集负面信息,避免用户在应用商店或者其他公共渠道给予差评,提供包含应用上下文的反馈信息以帮助开发人员快速解决问题。
实现方案
摇一摇触发功能:
通过查阅iOS框架文档的Responder chain
部分,我们发现在整个事件响应传递中,UIWindown这个类非常适合用来接收摇动事件。
@implementation UIWindow (ShakeFeedback)
- (BOOL)canBecomeFirstResponder {
return YES;
}
#pragma mark - 摇一摇动作处理
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event {
if (event.subtype == UIEventSubtypeMotionShake) {
// 摇动结束 执行反馈逻辑
}
}
@end
自动获取Bug产生时的界面截图:
- (UIImage *)screenshot {
UIView * view = [[UIApplication sharedApplication].windows firstObject];
// view frame为empty 降级
if (CGRectIsEmpty(view.frame)) {
return nil;
}
//绘图
//第一个参数表示区域大小。第二个参数表示是否是非透明的。如果需要显示半透明效果,需要传NO,否则传YES。第三个参数就是屏幕密度了。
UIGraphicsBeginImageContextWithOptions(view.frame.size, NO, [UIScreen mainScreen].scale);
//渲染
[view drawViewHierarchyInRect:view.bounds afterScreenUpdates:NO];
//生产图片
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
使用Bugly获取并上传移动设备的硬件参数(型号,系统版本,屏幕分辨率等)和应用运行上下文数据(如:网络状态,用户数据,日志信息,用户操作步骤等):
NSError *error = [[NSError alloc] initWithDomain:@"用户触发了主动反馈" code:0 userInfo:@{@"msgStr":msgStr?msgStr:@""}];
[Bugly reportError:error];
结合原有的IM对话反馈渠道,可以直接对用户发起沟通:
MOATabBarController *tabBarController =[IMMessageHelper getTabBarController];
[CoredataManager getObjWithSID:@(PidPublicAccount) andEntryName:kPersonEntryName andCallback:^(id result) {
if ([result isKindOfClass:[MOAPerson class]]) {
[result getIMSessionWithHandle:^(id sessionResult) {
IMCHatViewController *chatViewController = [[IMCHatViewController alloc]init];
chatViewController.session = sessionResult;
chatViewController.backToPreviewPageWhenPop = YES;
[MOASpeechActionHelper dismissAllPresentViews];
[tabBarController.navigationController pushViewController:chatViewController animated:YES];
if (self.textView.formatText.length) {
NSDictionary *dict = [IMMessageCell genertateDictWithContent:self.textView.formatText andType:IMContentTypeText option:kThumbOptionIM];
[[CoredataManager defaultInstance] updateDirectlyObject:sessionResult withData:@{@"method":@"newMsg",
@"newValueDict":dict}];
}
for (LMFeedbackMediaModel *model in self.arrDataSources) {
if (model.image) {
NSData *data = UIImageJPEGRepresentation(model.image, 1);
NSDictionary *dict = [IMMessageCell genertateDictWithContent:data andType:IMContentTypePicture option:kThumbOptionIM];
[[CoredataManager defaultInstance] updateDirectlyObject:sessionResult withData:@{@"method":@"newMsg",
@"newValueDict":dict,}];
}
}
}];
}
}];