SwiftUI框架详细解析 (十) —— 基于SwiftUI构建各种自定义图表(一)

版本记录

版本号 时间
V1.0 2020.01.10 星期五

前言

今天翻阅苹果的API文档,发现多了一个框架SwiftUI,这里我们就一起来看一下这个框架。感兴趣的看下面几篇文章。
1. SwiftUI框架详细解析 (一) —— 基本概览(一)
2. SwiftUI框架详细解析 (二) —— 基于SwiftUI的闪屏页的创建(一)
3. SwiftUI框架详细解析 (三) —— 基于SwiftUI的闪屏页的创建(二)
4. SwiftUI框架详细解析 (四) —— 使用SwiftUI进行苹果登录(一)
5. SwiftUI框架详细解析 (五) —— 使用SwiftUI进行苹果登录(二)
6. SwiftUI框架详细解析 (六) —— 基于SwiftUI的导航的实现(一)
7. SwiftUI框架详细解析 (七) —— 基于SwiftUI的导航的实现(二)
8. SwiftUI框架详细解析 (八) —— 基于SwiftUI的动画的实现(一)
9. SwiftUI框架详细解析 (九) —— 基于SwiftUI的动画的实现(二)

开始

首先看下主要内容:

主要内容:在本SwiftUI教程中,您将学习如何构建各种自定义图表,以有效地为用户建模iOS应用数据。

下面看下写作环境

Swift 5, iOS 13, Xcode 11

下面就是正文了。

图表是向用户显示数据的绝佳方法。 它们帮助用户掌握大量信息中固有的关系。 您可以使用图表来吸引人们注意趋势,弄清原因并帮助用户真正地可视化信息。

在本SwiftUI教程中,您将学习如何创建各种自定义图表来帮助可视化应用程序的数据。

尽管SwiftUI不提供本机图表库,但它具有丰富的图形功能,可用于构建自定义图表。 在本教程中,您将向应用程序添加图表,以显示大雾山国家公园(Great Smoky Mountains National Park)及其周围多个气象站的历史气象数据。

1. Why Use a Chart?

查看一些数据可能会很有启发性,但是盯着一长串数字并不是获得洞察力的最佳方法。 数字列表并不能使您更容易了解某个月的温暖程度或确定最干旱的月份。

以图形方式呈现信息时,大多数人都可以更轻松地掌握信息。 图表可以提供旨在告知查看者的数据的图形表示。

Charts vs. Graphs

人们经常互换使用术语“图表”和“图形”,但他们不是同一回事。

图形表示出值之间的任何关系。 一个简单的图形可以显示给定x的y值。 生成的曲线可能很漂亮,但没有提供任何可清晰的信息。

图表应该讲一个故事。 它使观看者更容易理解和解释,从而更好地理解数据。 简而言之,所有图表都是图形,但并非所有图形都是图表。

打开起始工程并运行

该应用程序显示五个站点的数据:

  • 北卡罗来纳州切诺基和田纳西州加特林堡(Cherokee, NC and Gatlinburg, TN):穿过公园的主要道路上的两个城市。
  • Newfound Gap:主要道路跨越的区域。
  • Townsend 5 S:公园西南部的区域。
  • Mount LeConte:公园里最高的山脉之一。

该数据集包含每个位置每天的降水,降雪和温度范围。

点按某个位置可显示有关该位置的信息,显示该位置的地图以及三个天气信息选项卡。这三个标签显示了每天的温度范围,每个月的总降水量以及每天有雪的降雪量。

首先,您需要在应用中添加条形图,以显示降水量数据。


Refactoring for Charts

条形图为每个数据点提供一个条形图。每个条的长度代表一个数值,可以水平或垂直延伸以满足您的需求。

展开Tabs组,然后打开PrecipitationTab.swift。您会看到一个标准的SwiftUI List(),该整数从011之间循环,代表一年中的月份,并显示每个月的总降雨量。包含的帮助器函数将整数更改为月份名称,并对每个月份的数量求和。

右键单击空的Charts组,然后选择New File。选择SwiftUI视图,然后单击下一步。将新视图命名为PrecipitationChart

确保该组设置为Charts,然后单击Create。打开新文件。如果“画布”不可见,请从菜单中选择Editor ▸ Canvas以将其打开,以便查看进度。

PrecipitationChart结构体的顶部添加以下代码:

var measurements: [DayInfo]

您可以使用此变量将度量传递到图表中。 现在更新PrecipitationChart_Previews以传递度量以进行预览。 在这种情况下,您将传递Mt. LeConte的测量值。

PrecipitationChart(measurements: WeatherInformation()!.stations[2].measurements)

首先,您将在此新视图中复制现有函数。 首先,在measurements后添加两个帮助函数:

func sumPrecipitation(_ month: Int) -> Double {
  self.measurements.filter {
    Calendar.current.component(.month, from: $0.date) == month + 1
  }.reduce(0, { $0 + $1.precipitation })
}

func monthAbbreviationFromInt(_ month: Int) -> String {
  let ma = Calendar.current.shortMonthSymbols
  return ma[month]
}

sumPrecipitation(_ :)使用filter仅获取传递给函数的月份的度量。 它调整传入的整数(从零开始而不是从1开始)。 reduce计算这些测量的降水值之和。

monthAbbreviationFromInt(_ :)获取当前日历的缩写月份符号列表,并返回与传递的整数匹配的月份符号。

更新body以复制现有列表:

List(0..<12) { month in
  Text("\(self.monthAbbreviationFromInt(month)): " +
    "\(self.sumPrecipitation(month))\"")
}

打开PrecipitationTab.swift并删除不再需要的sumPrecipitation(_ :)monthAbbreviationFromInt(_ :)方法。 在body内部,使用对新视图的调用来替换Listenclosure

PrecipitationChart(measurements: station.measurements)

注意:运行该应用程序时,请确保在选择要查看结果的位置后位于Precipitation选项卡上。


Raising the SwiftUI Bar

SwiftUI包含多个形状视图,其中包括一个矩形形状,可以很好地构建条形图。 打开PrecipitationChart.swift并将其替换为:

// 1
HStack {
  // 2
  ForEach(0..<12) { month in
    // 3
    VStack {
      // 4
      Spacer()
      // 5
      Rectangle()
        .fill(Color.green)
        .frame(width: 20, height: CGFloat(self.sumPrecipitation(month)) * 15.0)
      // 6
      Text("\(self.monthAbbreviationFromInt(month))")
        .font(.footnote)
        .frame(height: 20)
    }
  }
}

以下是逐步进行的细分:

  • 1) 您已经创建了垂直条,因此您可以使用HStack在设备上水平排列子视图。
  • 2) 您可以使用ForEach遍历月份。
  • 3) 您可以对图表中的每个条使用VStack来垂直堆叠元素。
  • 4) 您可以在堆栈中的其他视图上指定大小,然后该Spacer将展开以填充剩余的空间。实际上,它告诉SwiftUI将空白放在VStack的顶部。
  • 5) 您使用Rectangle SwiftUI shape原语。它创建一个与视图包含的frame对齐的矩形,并用绿色填充它。您指定宽度和高度恒定的frame作为月份的总降水量(以英寸为单位)乘以15。一个月有1英寸的降雨会形成一个20点宽,15点长的矩形。一个有七英寸大雨的月份显示为一个矩形,宽20点,长105点。
  • 6) 您还为每个条提供标签,在这种情况下为一年的一个月。在堆栈的底部,“文本”视图包含相应月份的缩写名称,并带有.footnote字体和一个静态高度。提供静态高度可确保条形底部对齐。

Adding a drop more detail

通过利用SwiftUI提供的功能,您已经构建了不错的条形图。 外部HStack均匀分布图表的条形图,这有助于提高可读性。 条形图的高度显示一年中降雨的比例。

但是,该图表并未明确指出确切的降水量。 将以下代码添加到body中的Spacer后面以显示该数据:

Text("\(self.sumPrecipitation(month).stringToOneDecimal)")
  .font(.footnote)
  .rotationEffect(.degrees(-90))
  .offset(y: 35)
  .zIndex(1)

您已在每个栏中添加了文本视图。 使用Double类型的扩展方法,它显示该月的总降水量,四舍五入到小数点后一位。 您可以在DoubleExtension.swift中找到它。

文本视图的字体设置为与月份标签匹配,并逆时针旋转文本90度,使其平行于条形流动。 然后将视图向下偏移35个点,并将其放置在条形图内。

SwiftUI按照阅读顺序渲染视图。 这意味着降雨量通常会位于栅栏后面,因为它占用的空间相同。

zIndex属性设置为默认零值以外的值会告诉SwiftUI覆盖该默认顺序。 将其设置为1会告诉SwiftUI使用默认的zIndex(包括栏)在视图顶部绘制Text

构建并运行应用程序以测试此新文本视图。 然后去北卡罗来纳州切诺基站(Cherokee, NC),选择降水(precipitation tab)标签,看一个有趣的bug。 2018年7月几乎没有下雨,使得条形太短而无法包含其文字。

要修复此错误,您需要通过以下方式替换文本视图中的偏移量,从而对偏移量进行检查:

.offset(y: self.sumPrecipitation(month) < 2.4 ? 0 : 35)

如果一个月的降水量少于2.4英寸,这将导致条形图长36点,文本将保留在条形图的顶部。

很好!您现在已经成功地用条形图替换了列表。此图表使查看者可以查看所有原始列表数据,并获得更清晰的指南,以了解每个月的降水差异。

有了降水图,您就可以创建降雪的水平条形图了。


Building a Horizontal Bar Chart

烟山山脉(Smoky Mountains)是美国东部海拔最高的地区。但是,在那些海拔较高的地方,它们收到的积雪比您预期的要少。

雪的稀缺意味着像降水图那样按月分组的图表将在年初和年底显示变化而中间没有任何变化。取而代之的是,您将使用水平条形图绘制雪状图,该条形图仅显示一年中降雪的日子。

右键单击Xco​​de中的Charts组,然后选择New File。选择SwiftUI视图,然后单击Next

将新视图命名为SnowfallChart,并确保该组设置为Charts。单击Create并打开新文件。

您需要通过将以下代码添加到该结构的顶部来再次将measurements传递到该视图:

var measurements: [DayInfo]

您将使用Mount LeConte进行预览,因为它具有最多的降雪天和最大的降雪量。 将预览更改为:

SnowfallChart(measurements: WeatherInformation()!.stations[2].measurements)

下面,将body更改为以下:

// 1
List(measurements.filter { $0.snowfall > 0.0 }) { measurement in
  HStack {
    // 2
    Text("\(measurement.dateString)")
      .frame(width: 100, alignment: .trailing)
    // 3
    Rectangle()
      .fill(Color.blue)
      .frame(width: CGFloat(measurement.snowfall * 10.0), height: 5.0)
    // 4
    Spacer()
    Text("\(measurement.snowfall.stringToOneDecimal)\"")
  }
}

以下是分步细分:

  • 1) 您创建一个List,其中包含每个降雪测量的条目。
  • 2) 您从下雪的日期开始每一行。默认情况下,Text视图的大小适合其包含的文本,行的宽度却保持变化。应用恒定的宽度可确保条形图从每一行的相同水平位置开始。您将文本对齐到显示降雪量的条形开头旁边的frame.trailing一侧。
  • 3) 您将蓝色矩形用作条形柱。由于这是水平而不是垂直的图表,因此请为条形图赋予恒定的高度,并根据积雪量设置宽度。由于视图上的水平空间较少,因此与上一个图表相比,使用更少的点表示每一英寸的降雪。
  • 4) 在Spacer()填充栏后的空白区域之后,您将显示以英寸为单位的降雪量,再次舍入为十分之一英寸。

返回SnowfallTab.swift,使用对新视图的调用替换List及其在body内部的闭包:

SnowfallChart(measurements: station.measurements)

图表现在显示了一年的降雪量。 看看12月会有特别大的降雪。


Adding Grid Lines

由于降雪量差异很大,因此您可以通过添加网格线进一步阐明图表。 这些是在图表或图形上以恒定值放置的线。 这使观察者更容易测量条的长度。

首先,将SnowfallChartRectangle()的代码更改为:

ZStack {
  Rectangle()
    .fill(Color.blue)
    .frame(width: CGFloat(measurement.snowfall * 10.0), height: 5.0)
}

ZStack使您可以在同一空间中叠加多个子视图。 在这种情况下,您将覆盖条形图和网格线。 您将以1英寸的间隔绘制网格线,以达到16英寸的最大尺寸。

Rectangle之后的ZStack中添加以下代码:

ForEach(0..<17) { mark in
  Rectangle()
    .fill(Color.gray)
    .offset(x: CGFloat(mark) * 10.0)
    .frame(width: 1.0)
    .zIndex(1)
}

在这里,您为每个月的数据绘制了一个用灰色填充的矩形。 offset(x:y :)修饰符将每条线向右移动适当的量,然后设置一个宽度为1的frame,将矩形变成一条线。 再次设置RectanglezIndex,使其显示在条形顶部。

请注意,通过不为frame设置高度,将扩展为包含frame的视图的高度。 如果查看当前状态,您会发现有些不对劲。

网格线和条形不一定总是正好对齐。 默认情况下,ZStack将其子视图对齐在中心,但是您可以通过稍作修改来显式指定子视图的对齐方式。 将声明ZStack的行更改为:

ZStack(alignment: .leading) {

现在条形和网格都正确显示了

如果您使用许多网格线,则可以通过定期提供视觉提示来帮助查看器。 将对fill(_:style :)的调用更改为:

.fill(mark % 5 == 0 ? Color.black : Color.gray)

这使用Swift三元运算符,使用余数运算符将每五个指示器上黑。

现在,您已经获得了创建几个基本图表的经验,现在可以继续创建用于温度数据的更复杂的热图。


Creating a Heat Map

Charts组中创建一个新的SwiftUI视图,并将新视图命名为TemperatureChart。 打开TemperatureChart.swift并在结构的开头添加一个用于测量数据的变量。

var measurements: [DayInfo]

更改预览以提供以下信息:

TemperatureChart(measurements: WeatherInformation()!.stations[1].measurements)

该图表应传达全年每个站点的高温和低温。 您将需要使用一些辅助函数来实现这种可视化。 在Measurements变量之后,将以下方法添加到结构中:

func degreeHeight(_ height: CGFloat, range: Int) -> CGFloat {
  height / CGFloat(range)
}

func dayWidth(_ width: CGFloat, count: Int) -> CGFloat {
  width / CGFloat(count)
}

该图表将调整以适合视图,而不是使用通过反复试验确定的固定量。 对于图表,这两个函数计算在垂直方向上一度的温度下获取的点,在水平方向上计算一日的水平点。 这两个函数都将维度的大小除以元素数。 结果给出了视图中每个元素要使用的点数。

使用该结果,您可以确定给定日期和温度的视图中的点位置。 在前两个函数之后添加以下两个函数:

func dayOffset(_ date: Date, dWidth: CGFloat) -> CGFloat {
  CGFloat(Calendar.current.ordinality(of: .day, in: .year, for: date)!) * dWidth
}

func tempOffset(_ temperature: Double, degreeHeight: CGFloat) -> CGFloat {
  CGFloat(temperature + 10) * degreeHeight
}

dayOffset(_:dWidth :)从传入的日期计算一年中的日期,然后乘以dWidth参数。 这将计算水平位置以在视图中绘制此测量值。

tempOffset(_:degreeHeight :)进行类似的计算以获取给定温度的点。 由于温度范围是从-10度开始的,因此在相乘之前将温度加10。 这会将范围的底部移至零点。

现在将body更改为以下内容:

// 1
GeometryReader { reader in
  ForEach(self.measurements) { measurement in
    // 2
    Path { p in
      // 3
      let dWidth = self.dayWidth(reader.size.width, count: 365)
      let dHeight = self.degreeHeight(reader.size.height, range: 110)
      // 4
      let dOffset = self.dayOffset(measurement.date, dWidth: dWidth)
      let lowOffset = self.tempOffset(measurement.low, degreeHeight: dHeight)
      let highOffset = self.tempOffset(measurement.high, degreeHeight: dHeight)
      // 5
      p.move(to: CGPoint(x: dOffset, y: reader.size.height - lowOffset))
      p.addLine(to: CGPoint(x: dOffset, y: reader.size.height - highOffset))
      // 6
    }.stroke()
  }
}

这里有很多东西,但是函数简化了许多所需的计算。代码的工作方式如下:

  • 1) 您创建GeometryReader来包装图表。 GeometryReader展开以填充包含它的视图。该闭包还提供了GeometryProxy参数,该参数包含有关视图大小的信息。
    在先前的图表中,您使用了恒定大小来生成看起来正确的东西。现在,您可以使用带有早期函数的这些值来计算图表的最佳值。
  • 2) 路径Path提供了一种创建二维形状的方法。在这里,您将创建一条垂直线,连接每天的低温和高温。路径在SwiftUI中还具有一些独特功能,您可以在其中定义变量,从而简化路径点的计算。
  • 3) 在这里,您可以使用这两个函数使用GeometryReader中的尺寸,以1度温度和1天为单位计算尺寸。您使用的温度范围是110,因为-10100华氏度涵盖了今年数据中所有位置的温度范围。
  • 4) 现在,您可以使用这些功能确定日期的垂直点以及高温和低温。
  • 5) 这些线将路径移至低温点,并向高温添加线。垂直视图坐标从视图顶部开始,然后向下增加。当您希望点从底部开始并向上移动时,可以从reader.size.height中减去垂直位置以获得所需的位置。
  • 6) stroke()告诉SwiftUI以当前系统颜色概述您创建的路径。

打开TemperatureTab.swift并用它替换body以使用新视图:

VStack {
  Text("Temperatures for 2018")
  TemperatureChart(measurements: station.measurements)
}.padding()

构建并运行该应用程序。 选择任意位置,然后查看温度tab。 请注意,该图表可以适应较小的应用内视图和较大的预览。

图表的形状很好地显示了温度的变化,但看起来有些平淡。 接下来,通过将图表转换为热图来使其变得更加有趣,该热图使用颜色更清楚地指示温度。


Adding Heat Map Color

热图使用颜色以图形方式表示值。 天气图通常使用多种颜色来表示温度,低温时从紫色和蓝色阴影开始,低温时向黄色,橙色和红色阴影移动。 计算这些颜色和变化可能涉及一些复杂的数学运算,但此处不涉及。

SwiftUI中,您使用渐变表示颜色的过渡。 线性渐变可沿单个轴在两种或多种颜色之间创建平滑的颜色过渡。 在measurements之后和辅助函数之前,在TemperatureChart.swift中添加以下内容:

let tempGradient = Gradient(colors: [
  .purple,
  Color(red: 0, green: 0, blue: 139.0/255.0),
  .blue,
  Color(red: 30.0/255.0, green: 144.0/255.0, blue: 1.0),
  Color(red: 0, green: 191/255.0, blue: 1.0),
  Color(red: 135.0/255.0, green: 206.0/255.0, blue: 250.0/255.0),
  .green,
  .yellow,
  .orange,
  Color(red: 1.0, green: 140.0/255.0, blue: 0.0),
  .red,
  Color(red: 139.0/255.0, green: 0.0, blue: 0.0)
])

这定义了一个由12种颜色组成的渐变,以110度的温度范围以十度的增量均匀划分,从紫色(-10度)到深红色(100度)。

现在,在主体视图的注释六处将stroke()更改为:

.stroke(LinearGradient(
  gradient: self.tempGradient,
  startPoint: UnitPoint(x: 0.0, y: 1.0),
  endPoint: UnitPoint(x: 0.0, y: 0.0)))

您可以使用先前定义的渐变色将纯色替换为线性渐变。 使用startPointendPoint参数,您可以执行几乎神奇的事情。

这两个参数都是UnitPoint,它们以点无关的方式定义空间,其中0.01.0标记视图的边缘。 每个方向的零点位于原点:视图的左上角。

您可以将渐变的起点设置在视图的左下角,将端点设置在视图的左上角。 由于它是线性渐变,因此渐变仅在垂直方向上变化。 每种颜色在每个点的整个视图中水平延伸。

将其应用于路径意味着梯度仅显示在stroked部分:低温和高温之间的范围。


Adding Grid Lines and Labels

现在剩下的就是通过添加网格线(类似于您在条形图中所做的操作)使观看者的视觉看起来更容易一些。 在TemperatureChart.swift中的现有辅助函数之后添加以下辅助函数:

func tempLabelOffset(_ line: Int, height: CGFloat) -> CGFloat {
  height - self.tempOffset(
    Double(line * 10),
    degreeHeight: self.degreeHeight(height, range: 110))
}

这会将网格划分为十度的块,并传递一个整数,该整数表示该块的起始温度除以十,再加上视图的总高度。 该函数计算适当的垂直偏移。

body中的ForEach循环的右括号后添加以下代码以绘制温度网格线和标签:

// 1
ForEach(-1..<11) { line in
  // 2
  Group {
    Path { path in
      let y = self.tempLabelOffset(line, height: reader.size.height)
      path.move(to: CGPoint(x: 0, y: y))
      path.addLine(to: CGPoint(x: reader.size.width, y: y))
      // 4
    }.stroke(line == 0 ? Color.black : Color.gray)
    // 5
    if line >= 0 {
      Text("\(line * 10)°")
        .offset(x: 10, y: self.tempLabelOffset(line, height: reader.size.height))
    }
  }
}

这是新代码的细分:

  • 1) 您可以在-110的范围内循环,代表-10100华氏度的温度。
  • 2) Group视图在SwiftUI中起到了一些粘合作用,它结合了其子视图,但不直接呈现元素。 在这里,它允许您在循环内使用PathText()视图。
  • 3) 您可以使用刚添加的函数来计算该线的温度位置。 然后,在该垂直位置从视图的左侧到右侧水平绘制线。
  • 4) 您将大多数网格线绘制为灰色。 为了使零度线突出,您可以将其显示为黑色。
  • 5) 对于除第一条网格线以外的所有文本,您都将添加一个文本标签。 由于您不再位于Path闭包内,因此需要重新计算该线所代表的温度的位置。 您再次使用tempLabelOffset(_:height :)函数来计算垂直位置。

完成温度后,您需要数月的指示器和标签。 在现有的辅助函数之后添加以下两个辅助函数:

func offsetFirstOfMonth(_ month: Int, width: CGFloat) -> CGFloat {
  let dateFormatter = DateFormatter()
  dateFormatter.dateFormat = "M/d/yyyy"
  let foM = dateFormatter.date(from: "\(month)/1/2018")!
  let dayWidth = self.dayWidth(width, count: 365)
  return self.dayOffset(foM, dWidth: dayWidth)
}

func monthAbbreviationFromInt(_ month: Int) -> String {
  let ma = Calendar.current.shortMonthSymbols
  return ma[month - 1]
}

添加以下代码,将前一个ForEach循环的右括号后的月份网格线和标签添加到body的末尾:

ForEach(1..<13) { month in
  Group {
    Path { path in
      let dOffset = self.offsetFirstOfMonth(month, width: reader.size.width)

      path.move(to: CGPoint(x: dOffset, y: reader.size.height))
      path.addLine(to: CGPoint(x: dOffset, y: 0))
    }.stroke(Color.gray)
    Text("\(self.monthAbbreviationFromInt(month))")
      .font(.subheadline)
      .offset(
        x: self.offsetFirstOfMonth(month, width: reader.size.width) +
          5 * self.dayWidth(reader.size.width, count: 365),
        y: reader.size.height - 25.0)
  }
}

这里没有您以前没有用过的东西。 与以前一样,Group会包装网格线和月份标签。 然后,在与每个月的第一天相对应的偏移处绘制一条垂直线。

然后,您将获得每个月的文本缩写,并以相同的偏移量加上一点点的偏移量进行绘制,以将文本移动到月中。 您将获得每个月的文本缩写,并以相同的偏移量加上一点点的偏移量进行绘制,以将文本移至月中。

现在,您的图表可以很好地概述每个位置的温度范围。 每条垂直线的顶部和底部均与颜色相结合,以清楚地显示一年中不同时间的温度。 网格线和标签可帮助观看者确定一年中的某个时间或温度范围。

如果您想了解更多信息,那么对于所有UI内容,Apple人机界面指南都是一个不错的起点。 您将在《人机界面指南》中找到有关Charts的简短部分。 为图表选择颜色时,还应该阅读Color准则。

后记

本篇主要讲述了基于SwiftUI构建各种自定义图表,感兴趣的给个赞或者关注~~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容